wickra_core/indicators/
trima.rs1use crate::error::{Error, Result};
4use crate::traits::Indicator;
5
6use super::Sma;
7
8#[derive(Debug, Clone)]
30pub struct Trima {
31 period: usize,
32 inner: Sma,
33 outer: Sma,
34}
35
36impl Trima {
37 pub fn new(period: usize) -> Result<Self> {
43 if period == 0 {
44 return Err(Error::PeriodZero);
45 }
46 let (n1, n2) = if period % 2 == 1 {
47 (period.div_ceil(2), period.div_ceil(2))
48 } else {
49 (period / 2, period / 2 + 1)
50 };
51 Ok(Self {
52 period,
53 inner: Sma::new(n1)?,
54 outer: Sma::new(n2)?,
55 })
56 }
57
58 pub const fn period(&self) -> usize {
60 self.period
61 }
62
63 pub fn value(&self) -> Option<f64> {
65 self.outer.value()
66 }
67}
68
69impl Indicator for Trima {
70 type Input = f64;
71 type Output = f64;
72
73 fn update(&mut self, input: f64) -> Option<f64> {
74 if !input.is_finite() {
75 return self.outer.value();
78 }
79 match self.inner.update(input) {
81 Some(v) => self.outer.update(v),
82 None => None,
83 }
84 }
85
86 fn reset(&mut self) {
87 self.inner.reset();
88 self.outer.reset();
89 }
90
91 fn warmup_period(&self) -> usize {
92 self.period
93 }
94
95 fn is_ready(&self) -> bool {
96 self.outer.is_ready()
97 }
98
99 fn name(&self) -> &'static str {
100 "TRIMA"
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::traits::BatchExt;
108 use approx::assert_relative_eq;
109
110 #[test]
111 fn new_rejects_zero_period() {
112 assert!(matches!(Trima::new(0), Err(Error::PeriodZero)));
113 }
114
115 #[test]
119 fn accessors_and_metadata() {
120 let mut t = Trima::new(5).unwrap();
121 assert_eq!(t.period(), 5);
122 assert_eq!(t.name(), "TRIMA");
123 assert_eq!(t.value(), None);
124 for i in 1..=t.warmup_period() {
125 t.update(f64::from(u32::try_from(i).unwrap()));
126 }
127 assert!(t.value().is_some());
128 }
129
130 #[test]
131 fn odd_period_reference_values() {
132 let mut trima = Trima::new(5).unwrap();
135 let out = trima.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
136 assert_eq!(out[0], None);
137 assert_eq!(out[3], None);
138 assert_relative_eq!(out[4].unwrap(), 3.0, epsilon = 1e-12);
139 assert_relative_eq!(out[5].unwrap(), 4.0, epsilon = 1e-12);
140 assert_relative_eq!(out[6].unwrap(), 5.0, epsilon = 1e-12);
141 }
142
143 #[test]
144 fn first_emission_at_warmup_period() {
145 let mut trima = Trima::new(6).unwrap();
147 let out = trima.batch(&(1..=10).map(f64::from).collect::<Vec<_>>());
148 assert_eq!(trima.warmup_period(), 6);
149 for v in out.iter().take(5) {
150 assert!(v.is_none());
151 }
152 assert!(out[5].is_some());
153 }
154
155 #[test]
156 fn constant_series_yields_the_constant() {
157 let mut trima = Trima::new(7).unwrap();
158 let out = trima.batch(&[42.0; 20]);
159 for x in out.iter().skip(6) {
160 assert_relative_eq!(x.unwrap(), 42.0, epsilon = 1e-12);
161 }
162 }
163
164 #[test]
165 fn ignores_non_finite_input() {
166 let mut trima = Trima::new(5).unwrap();
167 let ready = trima.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
168 let last = ready[4];
169 assert!(last.is_some());
170 assert_eq!(trima.update(f64::NAN), last);
171 }
172
173 #[test]
174 fn reset_clears_state() {
175 let mut trima = Trima::new(5).unwrap();
176 trima.batch(&(1..=10).map(f64::from).collect::<Vec<_>>());
177 assert!(trima.is_ready());
178 trima.reset();
179 assert!(!trima.is_ready());
180 assert_eq!(trima.update(1.0), None);
181 }
182
183 #[test]
184 fn batch_equals_streaming() {
185 let prices: Vec<f64> = (1..=40).map(f64::from).collect();
186 let batch = Trima::new(8).unwrap().batch(&prices);
187 let mut b = Trima::new(8).unwrap();
188 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
189 assert_eq!(batch, streamed);
190 }
191}