wickra_core/indicators/
tsi.rs1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6use super::Ema;
7
8#[derive(Debug, Clone)]
36pub struct Tsi {
37 long: usize,
38 short: usize,
39 prev_price: Option<f64>,
40 ema_long_mom: Ema,
41 ema_short_mom: Ema,
42 ema_long_abs: Ema,
43 ema_short_abs: Ema,
44 current: Option<f64>,
45}
46
47impl Tsi {
48 pub fn new(long: usize, short: usize) -> Result<Self> {
54 if long == 0 || short == 0 {
55 return Err(Error::PeriodZero);
56 }
57 Ok(Self {
58 long,
59 short,
60 prev_price: None,
61 ema_long_mom: Ema::new(long)?,
62 ema_short_mom: Ema::new(short)?,
63 ema_long_abs: Ema::new(long)?,
64 ema_short_abs: Ema::new(short)?,
65 current: None,
66 })
67 }
68
69 pub const fn periods(&self) -> (usize, usize) {
71 (self.long, self.short)
72 }
73
74 pub const fn value(&self) -> Option<f64> {
76 self.current
77 }
78}
79
80impl Indicator for Tsi {
81 type Input = f64;
82 type Output = f64;
83
84 #[inline]
85 fn update(&mut self, input: f64) -> Option<f64> {
86 if !input.is_finite() {
87 return None;
89 }
90 let Some(prev) = self.prev_price else {
91 self.prev_price = Some(input);
92 return None;
93 };
94 self.prev_price = Some(input);
95
96 let momentum = input - prev;
97 let ds_mom = self
98 .ema_long_mom
99 .update(momentum)
100 .and_then(|v| self.ema_short_mom.update(v));
101 let ds_abs = self
102 .ema_long_abs
103 .update(momentum.abs())
104 .and_then(|v| self.ema_short_abs.update(v));
105
106 match (ds_mom, ds_abs) {
107 (Some(m), Some(a)) => {
108 let tsi = if a == 0.0 {
109 0.0
111 } else {
112 100.0 * m / a
113 };
114 self.current = Some(tsi);
115 Some(tsi)
116 }
117 _ => None,
118 }
119 }
120
121 fn reset(&mut self) {
122 self.prev_price = None;
123 self.ema_long_mom.reset();
124 self.ema_short_mom.reset();
125 self.ema_long_abs.reset();
126 self.ema_short_abs.reset();
127 self.current = None;
128 }
129
130 #[inline]
131 fn warmup_period(&self) -> usize {
132 self.long + self.short
133 }
134
135 #[inline]
136 fn is_ready(&self) -> bool {
137 self.current.is_some()
138 }
139
140 #[inline]
141 fn name(&self) -> &'static str {
142 "TSI"
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::traits::BatchExt;
150 use approx::assert_relative_eq;
151
152 #[test]
153 fn new_rejects_zero_period() {
154 assert!(matches!(Tsi::new(0, 13), Err(Error::PeriodZero)));
155 assert!(matches!(Tsi::new(25, 0), Err(Error::PeriodZero)));
156 }
157
158 #[test]
162 fn accessors_and_metadata() {
163 let mut tsi = Tsi::new(25, 13).unwrap();
164 assert_eq!(tsi.periods(), (25, 13));
165 assert_eq!(tsi.name(), "TSI");
166 assert_eq!(tsi.value(), None);
167 for i in 1..=tsi.warmup_period() {
168 tsi.update(100.0 + f64::from(u32::try_from(i).unwrap()));
169 }
170 assert!(tsi.value().is_some());
171 }
172
173 #[test]
174 fn first_emission_at_warmup_period() {
175 let mut tsi = Tsi::new(5, 3).unwrap();
176 assert_eq!(tsi.warmup_period(), 8);
177 let out = tsi.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
178 for v in out.iter().take(7) {
179 assert!(v.is_none());
180 }
181 assert!(out[7].is_some());
182 }
183
184 #[test]
185 fn pure_uptrend_saturates_at_plus_100() {
186 let mut tsi = Tsi::new(5, 3).unwrap();
188 let out = tsi.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
189 for v in out.iter().skip(8).flatten() {
190 assert_relative_eq!(*v, 100.0, epsilon = 1e-9);
191 }
192 }
193
194 #[test]
195 fn pure_downtrend_saturates_at_minus_100() {
196 let mut tsi = Tsi::new(5, 3).unwrap();
197 let out = tsi.batch(&(1..=40).rev().map(f64::from).collect::<Vec<_>>());
198 for v in out.iter().skip(8).flatten() {
199 assert_relative_eq!(*v, -100.0, epsilon = 1e-9);
200 }
201 }
202
203 #[test]
204 fn constant_series_yields_zero() {
205 let mut tsi = Tsi::new(5, 3).unwrap();
206 let out = tsi.batch(&[50.0; 40]);
207 for v in out.iter().skip(8).flatten() {
208 assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
209 }
210 }
211
212 #[test]
213 fn ignores_non_finite_input() {
214 let mut tsi = Tsi::new(5, 3).unwrap();
215 let out = tsi.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
216 let last = *out.last().unwrap();
217 assert!(last.is_some());
218 assert_eq!(tsi.update(f64::NAN), None);
219 assert_eq!(tsi.update(f64::INFINITY), None);
220 }
221
222 #[test]
223 fn reset_clears_state() {
224 let mut tsi = Tsi::new(5, 3).unwrap();
225 tsi.batch(&(1..=40).map(f64::from).collect::<Vec<_>>());
226 assert!(tsi.is_ready());
227 tsi.reset();
228 assert!(!tsi.is_ready());
229 assert_eq!(tsi.update(1.0), None);
230 }
231
232 #[test]
233 fn batch_equals_streaming() {
234 let prices: Vec<f64> = (1..=80)
235 .map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 9.0)
236 .collect();
237 let batch = Tsi::new(13, 7).unwrap().batch(&prices);
238 let mut b = Tsi::new(13, 7).unwrap();
239 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
240 assert_eq!(batch, streamed);
241 }
242}