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 period: usize,
31}
32
33impl Trix {
34 pub fn new(period: usize) -> Result<Self> {
37 Ok(Self {
38 ema1: Ema::new(period)?,
39 ema2: Ema::new(period)?,
40 ema3: Ema::new(period)?,
41 prev_tr: None,
42 period,
43 })
44 }
45
46 pub const fn period(&self) -> usize {
48 self.period
49 }
50}
51
52impl Indicator for Trix {
53 type Input = f64;
54 type Output = f64;
55
56 fn update(&mut self, input: f64) -> Option<f64> {
57 let e1 = self.ema1.update(input)?;
58 let e2 = self.ema2.update(e1)?;
59 let e3 = self.ema3.update(e2)?;
60 match self.prev_tr {
61 Some(prev) if prev != 0.0 => {
62 let trix = 100.0 * (e3 - prev) / prev;
63 self.prev_tr = Some(e3);
64 Some(trix)
65 }
66 Some(_) => {
67 self.prev_tr = Some(e3);
68 Some(0.0)
69 }
70 None => {
71 self.prev_tr = Some(e3);
72 None
73 }
74 }
75 }
76
77 fn reset(&mut self) {
78 self.ema1.reset();
79 self.ema2.reset();
80 self.ema3.reset();
81 self.prev_tr = None;
82 }
83
84 fn warmup_period(&self) -> usize {
85 3 * self.period - 1
87 }
88
89 fn is_ready(&self) -> bool {
90 self.prev_tr.is_some() && self.ema3.is_ready()
91 }
92
93 fn name(&self) -> &'static str {
94 "TRIX"
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::traits::BatchExt;
102 use approx::assert_relative_eq;
103
104 #[test]
105 fn constant_series_yields_zero_trix() {
106 let mut trix = Trix::new(5).unwrap();
107 let out = trix.batch(&[100.0_f64; 80]);
108 let last = out.iter().rev().flatten().next().unwrap();
109 assert_relative_eq!(*last, 0.0, epsilon = 1e-9);
110 }
111
112 #[test]
113 fn rising_series_eventually_positive_trix() {
114 let prices: Vec<f64> = (1..=200).map(f64::from).collect();
115 let mut trix = Trix::new(5).unwrap();
116 let last = trix.batch(&prices).into_iter().flatten().last().unwrap();
117 assert!(last > 0.0);
118 }
119
120 #[test]
121 fn batch_equals_streaming() {
122 let prices: Vec<f64> = (1..=80).map(|i| f64::from(i) * 1.3).collect();
123 let mut a = Trix::new(7).unwrap();
124 let mut b = Trix::new(7).unwrap();
125 assert_eq!(
126 a.batch(&prices),
127 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
128 );
129 }
130
131 #[test]
132 fn reset_clears_state() {
133 let mut trix = Trix::new(5).unwrap();
134 trix.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
135 assert!(trix.is_ready());
136 trix.reset();
137 assert!(!trix.is_ready());
138 }
139
140 #[test]
141 fn rejects_zero_period() {
142 assert!(Trix::new(0).is_err());
143 }
144
145 #[test]
149 fn accessors_and_metadata() {
150 let trix = Trix::new(5).unwrap();
151 assert_eq!(trix.period(), 5);
152 assert_eq!(trix.warmup_period(), 14);
154 assert_eq!(trix.name(), "TRIX");
155 }
156
157 #[test]
164 fn zero_input_series_yields_zero_trix() {
165 let mut trix = Trix::new(3).unwrap();
166 let out = trix.batch(&[0.0_f64; 20]);
167 let last = out.into_iter().flatten().last().expect("emits");
168 assert_eq!(last, 0.0);
169 }
170}