wickra_core/indicators/
trix.rs1use crate::error::Result;
4use crate::indicators::ema::Ema;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
25pub struct Trix {
26 ema1: Ema,
27 ema2: Ema,
28 ema3: Ema,
29 prev_tr: Option<f64>,
30 has_emitted: bool,
35 period: usize,
36}
37
38impl Trix {
39 pub fn new(period: usize) -> Result<Self> {
42 Ok(Self {
43 ema1: Ema::new(period)?,
44 ema2: Ema::new(period)?,
45 ema3: Ema::new(period)?,
46 prev_tr: None,
47 has_emitted: false,
48 period,
49 })
50 }
51
52 pub const fn period(&self) -> usize {
54 self.period
55 }
56}
57
58impl Indicator for Trix {
59 type Input = f64;
60 type Output = f64;
61
62 #[inline]
63 fn update(&mut self, input: f64) -> Option<f64> {
64 let e1 = self.ema1.update(input)?;
65 let e2 = self.ema2.update(e1)?;
66 let e3 = self.ema3.update(e2)?;
67 match self.prev_tr {
68 Some(prev) if prev != 0.0 => {
69 let trix = 100.0 * (e3 - prev) / prev;
70 self.prev_tr = Some(e3);
71 self.has_emitted = true;
72 Some(trix)
73 }
74 Some(_) => {
75 self.prev_tr = Some(e3);
76 self.has_emitted = true;
77 Some(0.0)
78 }
79 None => {
80 self.prev_tr = Some(e3);
81 None
82 }
83 }
84 }
85
86 fn reset(&mut self) {
87 self.ema1.reset();
88 self.ema2.reset();
89 self.ema3.reset();
90 self.prev_tr = None;
91 self.has_emitted = false;
92 }
93
94 #[inline]
95 fn warmup_period(&self) -> usize {
96 3 * self.period - 1
98 }
99
100 #[inline]
101 fn is_ready(&self) -> bool {
102 self.has_emitted
103 }
104
105 #[inline]
106 fn name(&self) -> &'static str {
107 "TRIX"
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::traits::BatchExt;
115 use approx::assert_relative_eq;
116
117 #[test]
118 fn constant_series_yields_zero_trix() {
119 let mut trix = Trix::new(5).unwrap();
120 let out = trix.batch(&[100.0_f64; 80]);
121 let last = out.iter().rev().flatten().next().unwrap();
122 assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
123 }
124
125 #[test]
126 fn rising_series_eventually_positive_trix() {
127 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
128 let mut trix = Trix::new(5).unwrap();
129 let last = trix.batch(&prices).into_iter().flatten().last().unwrap();
130 assert!(last > 0.0);
131 }
132
133 #[test]
134 fn batch_equals_streaming() {
135 let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 1.3).collect();
136 let mut a = Trix::new(7).unwrap();
137 let mut b = Trix::new(7).unwrap();
138 assert_eq!(
139 a.batch(&prices),
140 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
141 );
142 }
143
144 #[test]
145 fn reset_clears_state() {
146 let mut trix = Trix::new(5).unwrap();
147 trix.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
148 assert!(trix.is_ready());
149 trix.reset();
150 assert!(!trix.is_ready());
151 }
152
153 #[test]
154 fn rejects_zero_period() {
155 assert!(Trix::new(0).is_err());
156 }
157
158 #[test]
163 fn readiness_flips_exactly_when_the_first_value_lands() {
164 let prices: Vec<f64> = (1..=80).map(|i| 100.0 + f64::from(i) * 0.7).collect();
165 let mut trix = Trix::new(3).unwrap();
166 let mut first = None;
167 for (i, price) in prices.iter().enumerate() {
168 let out = trix.update(*price);
169 assert_eq!(out.is_some(), trix.is_ready());
170 if out.is_some() && first.is_none() {
171 first = Some(i);
172 }
173 }
174 assert_eq!(first.unwrap() + 1, trix.warmup_period());
175 }
176
177 #[test]
181 fn accessors_and_metadata() {
182 let trix = Trix::new(5).unwrap();
183 assert_eq!(trix.period(), 5);
184 assert_eq!(trix.warmup_period(), 14);
186 assert_eq!(trix.name(), "TRIX");
187 }
188
189 #[test]
196 fn zero_input_series_yields_zero_trix() {
197 let mut trix = Trix::new(3).unwrap();
198 let out = trix.batch(&[0.0_f64; 20]);
199 let last = out.into_iter().flatten().last().expect("emits");
200 assert_eq!(last, 0.0);
201 }
202}