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 fn warmup_period(&self) -> usize {
149 self.sma_period + self.dev_period - 1
153 }
154
155 fn is_ready(&self) -> bool {
156 self.last.is_some()
157 }
158
159 fn name(&self) -> &'static str {
160 "TII"
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use crate::traits::BatchExt;
168 use approx::assert_relative_eq;
169
170 #[test]
171 fn rejects_zero_period() {
172 assert!(matches!(Tii::new(0, 10), Err(Error::PeriodZero)));
173 assert!(matches!(Tii::new(10, 0), Err(Error::PeriodZero)));
174 }
175
176 #[test]
177 fn accessors_and_metadata() {
178 let mut t = Tii::new(60, 30).unwrap();
179 assert_eq!(t.periods(), (60, 30));
180 assert_eq!(t.warmup_period(), 89);
181 assert_eq!(t.name(), "TII");
182 assert!(t.value().is_none());
183 let prices: Vec<f64> = (1..=100).map(|i| 100.0 + f64::from(i)).collect();
184 for &p in &prices {
185 t.update(p);
186 }
187 assert!(t.value().is_some());
188 }
189
190 #[test]
191 fn first_emission_at_warmup_period() {
192 let prices: Vec<f64> = (1..=30)
193 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 5.0)
194 .collect();
195 let mut t = Tii::new(5, 4).unwrap();
196 let out = t.batch(&prices);
197 let warmup = 5 + 4 - 1; for v in out.iter().take(warmup - 1) {
199 assert!(v.is_none());
200 }
201 assert!(out[warmup - 1].is_some());
202 }
203
204 #[test]
205 fn pure_uptrend_saturates_at_100() {
206 let prices: Vec<f64> = (1..=80).map(|i| 100.0 + f64::from(i)).collect();
209 let mut t = Tii::new(10, 5).unwrap();
210 let last = t.batch(&prices).into_iter().flatten().last().unwrap();
211 assert_relative_eq!(last, 100.0, epsilon = 1e-9);
212 }
213
214 #[test]
215 fn pure_downtrend_falls_to_zero() {
216 let prices: Vec<f64> = (1..=80).rev().map(|i| 100.0 + f64::from(i)).collect();
217 let mut t = Tii::new(10, 5).unwrap();
218 let last = t.batch(&prices).into_iter().flatten().last().unwrap();
219 assert_relative_eq!(last, 0.0, epsilon = 1e-9);
220 }
221
222 #[test]
223 fn constant_series_yields_neutral_50() {
224 let mut t = Tii::new(5, 4).unwrap();
227 let last = t
228 .batch(&[10.0_f64; 30])
229 .into_iter()
230 .flatten()
231 .last()
232 .unwrap();
233 assert_eq!(last, 50.0);
234 }
235
236 #[test]
237 fn output_bounded_in_unit_interval() {
238 let prices: Vec<f64> = (0..200)
239 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 6.0 + (f64::from(i) * 0.07).cos() * 3.0)
240 .collect();
241 let mut t = Tii::new(20, 10).unwrap();
242 for v in t.batch(&prices).into_iter().flatten() {
243 assert!((0.0..=100.0).contains(&v));
244 }
245 }
246
247 #[test]
248 fn batch_equals_streaming() {
249 let prices: Vec<f64> = (0..120)
250 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 5.0)
251 .collect();
252 let mut a = Tii::new(20, 10).unwrap();
253 let mut b = Tii::new(20, 10).unwrap();
254 assert_eq!(
255 a.batch(&prices),
256 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
257 );
258 }
259
260 #[test]
261 fn reset_clears_state() {
262 let mut t = Tii::new(5, 4).unwrap();
263 t.batch(&(1..=30).map(f64::from).collect::<Vec<_>>());
264 assert!(t.is_ready());
265 t.reset();
266 assert!(!t.is_ready());
267 assert_eq!(t.update(1.0), None);
268 }
269}