wickra_core/indicators/
t3.rs1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6use super::Ema;
7
8#[derive(Debug, Clone)]
43pub struct T3 {
44 period: usize,
45 v: f64,
46 c1: f64,
47 c2: f64,
48 c3: f64,
49 c4: f64,
50 e1: Ema,
51 e2: Ema,
52 e3: Ema,
53 e4: Ema,
54 e5: Ema,
55 e6: Ema,
56 current: Option<f64>,
57}
58
59impl T3 {
60 pub fn new(period: usize, v: f64) -> Result<Self> {
67 if period == 0 {
68 return Err(Error::PeriodZero);
69 }
70 if period > crate::error::MAX_PERIOD {
71 return Err(Error::InvalidPeriod {
72 message: crate::error::PERIOD_ABOVE_MAX,
73 });
74 }
75 if !v.is_finite() || !(0.0..=1.0).contains(&v) {
76 return Err(Error::InvalidPeriod {
77 message: "T3 volume factor must be a finite value in [0.0, 1.0]",
78 });
79 }
80 let v2 = v * v;
81 let v3 = v2 * v;
82 Ok(Self {
83 period,
84 v,
85 c1: -v3,
86 c2: 3.0 * v2 + 3.0 * v3,
87 c3: -6.0 * v2 - 3.0 * v - 3.0 * v3,
88 c4: 1.0 + 3.0 * v + v3 + 3.0 * v2,
89 e1: Ema::new(period)?,
90 e2: Ema::new(period)?,
91 e3: Ema::new(period)?,
92 e4: Ema::new(period)?,
93 e5: Ema::new(period)?,
94 e6: Ema::new(period)?,
95 current: None,
96 })
97 }
98
99 pub const fn period(&self) -> usize {
101 self.period
102 }
103
104 pub const fn volume_factor(&self) -> f64 {
106 self.v
107 }
108
109 pub const fn value(&self) -> Option<f64> {
111 self.current
112 }
113}
114
115impl Indicator for T3 {
116 type Input = f64;
117 type Output = f64;
118
119 #[inline]
120 fn update(&mut self, input: f64) -> Option<f64> {
121 if !input.is_finite() {
122 return None;
124 }
125 let e1 = self.e1.update(input)?;
126 let e2 = self.e2.update(e1)?;
127 let e3 = self.e3.update(e2)?;
128 let e4 = self.e4.update(e3)?;
129 let e5 = self.e5.update(e4)?;
130 let e6 = self.e6.update(e5)?;
131 let out = self.c1 * e6 + self.c2 * e5 + self.c3 * e4 + self.c4 * e3;
132 self.current = Some(out);
133 Some(out)
134 }
135
136 fn reset(&mut self) {
137 self.e1.reset();
138 self.e2.reset();
139 self.e3.reset();
140 self.e4.reset();
141 self.e5.reset();
142 self.e6.reset();
143 self.current = None;
144 }
145
146 #[inline]
147 fn warmup_period(&self) -> usize {
148 6 * self.period - 5
149 }
150
151 #[inline]
152 fn is_ready(&self) -> bool {
153 self.current.is_some()
154 }
155
156 #[inline]
157 fn name(&self) -> &'static str {
158 "T3"
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::traits::BatchExt;
166 use approx::assert_relative_eq;
167
168 #[test]
169 fn new_rejects_zero_period() {
170 assert!(matches!(T3::new(0, 0.7), Err(Error::PeriodZero)));
171 }
172
173 #[test]
178 fn accessors_and_metadata() {
179 let mut t3 = T3::new(5, 0.7).unwrap();
180 assert_eq!(t3.period(), 5);
181 assert_relative_eq!(t3.volume_factor(), 0.7, epsilon = 1e-12);
182 assert_eq!(t3.name(), "T3");
183 assert_eq!(t3.value(), None);
184 for _ in 0..t3.warmup_period() {
185 t3.update(50.0);
186 }
187 assert!(t3.value().is_some());
188 }
189
190 #[test]
191 fn new_rejects_out_of_range_volume_factor() {
192 assert!(matches!(T3::new(5, -0.1), Err(Error::InvalidPeriod { .. })));
193 assert!(matches!(T3::new(5, 1.5), Err(Error::InvalidPeriod { .. })));
194 assert!(matches!(
195 T3::new(5, f64::NAN),
196 Err(Error::InvalidPeriod { .. })
197 ));
198 assert!(T3::new(5, 0.0).is_ok());
199 assert!(T3::new(5, 1.0).is_ok());
200 }
201
202 #[test]
203 fn coefficients_sum_to_one() {
204 for &v in &[0.0, 0.3, 0.7, 1.0] {
206 let t3 = T3::new(5, v).unwrap();
207 assert_relative_eq!(t3.c1 + t3.c2 + t3.c3 + t3.c4, 1.0, epsilon = 1e-12);
208 }
209 }
210
211 #[test]
212 fn first_emission_at_warmup_period() {
213 let mut t3 = T3::new(4, 0.7).unwrap();
214 assert_eq!(t3.warmup_period(), 6 * 4 - 5);
215 let out = t3.batch(&(1..=60).map(f64::from).collect::<Vec<_>>());
216 for v in out.iter().take(t3.warmup_period() - 1) {
217 assert!(v.is_none());
218 }
219 assert!(out[t3.warmup_period() - 1].is_some());
220 }
221
222 #[test]
223 fn constant_series_yields_the_constant() {
224 let mut t3 = T3::new(6, 0.7).unwrap();
225 let out = t3.batch(&[50.0; 80]);
226 let last = out.iter().rev().flatten().next().unwrap();
227 assert_relative_eq!(*last, 50.0, epsilon = 1e-9);
228 }
229
230 #[test]
231 fn zero_volume_factor_collapses_to_triple_cascaded_ema() {
232 let prices: Vec<f64> = (1..=80)
235 .map(|i| 100.0 + (f64::from(i) * 0.2).sin() * 9.0)
236 .collect();
237 let mut t3 = T3::new(5, 0.0).unwrap();
238 let got = t3.batch(&prices);
239
240 let mut e1 = Ema::new(5).unwrap();
241 let mut e2 = Ema::new(5).unwrap();
242 let mut e3 = Ema::new(5).unwrap();
243 let want: Vec<Option<f64>> = prices
244 .iter()
245 .map(|p| {
246 e1.update(*p)
247 .and_then(|a| e2.update(a))
248 .and_then(|b| e3.update(b))
249 })
250 .collect();
251
252 for i in (t3.warmup_period() - 1)..prices.len() {
253 assert_relative_eq!(got[i].unwrap(), want[i].unwrap(), epsilon = 1e-9);
254 }
255 }
256
257 #[test]
258 fn ignores_non_finite_input() {
259 let mut t3 = T3::new(4, 0.7).unwrap();
260 let out = t3.batch(&(1..=60).map(f64::from).collect::<Vec<_>>());
261 let last = *out.last().unwrap();
262 assert!(last.is_some());
263 assert_eq!(t3.update(f64::NAN), None);
264 assert_eq!(t3.update(f64::INFINITY), None);
265 }
266
267 #[test]
268 fn reset_clears_state() {
269 let mut t3 = T3::new(4, 0.7).unwrap();
270 t3.batch(&(1..=60).map(f64::from).collect::<Vec<_>>());
271 assert!(t3.is_ready());
272 t3.reset();
273 assert!(!t3.is_ready());
274 assert_eq!(t3.update(1.0), None);
275 }
276
277 #[test]
278 fn batch_equals_streaming() {
279 let prices: Vec<f64> = (1..=120)
280 .map(|i| 100.0 + (f64::from(i) * 0.25).sin() * 7.0)
281 .collect();
282 let batch = T3::new(7, 0.7).unwrap().batch(&prices);
283 let mut b = T3::new(7, 0.7).unwrap();
284 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
285 assert_eq!(batch, streamed);
286 }
287}