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 if period > crate::error::MAX_PERIOD {
47 return Err(Error::InvalidPeriod {
48 message: crate::error::PERIOD_ABOVE_MAX,
49 });
50 }
51 let (n1, n2) = if period % 2 == 1 {
52 (period.div_ceil(2), period.div_ceil(2))
53 } else {
54 (period / 2, period / 2 + 1)
55 };
56 Ok(Self {
57 period,
58 inner: Sma::new(n1)?,
59 outer: Sma::new(n2)?,
60 })
61 }
62
63 pub const fn period(&self) -> usize {
65 self.period
66 }
67
68 pub fn value(&self) -> Option<f64> {
70 self.outer.value()
71 }
72}
73
74impl Indicator for Trima {
75 type Input = f64;
76 type Output = f64;
77
78 #[inline]
79 fn update(&mut self, input: f64) -> Option<f64> {
80 if !input.is_finite() {
81 return None;
84 }
85 match self.inner.update(input) {
87 Some(v) => self.outer.update(v),
88 None => None,
89 }
90 }
91
92 fn reset(&mut self) {
93 self.inner.reset();
94 self.outer.reset();
95 }
96
97 #[inline]
98 fn warmup_period(&self) -> usize {
99 self.period
100 }
101
102 #[inline]
103 fn is_ready(&self) -> bool {
104 self.outer.is_ready()
105 }
106
107 #[inline]
108 fn name(&self) -> &'static str {
109 "TRIMA"
110 }
111
112 fn batch_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
118 assert_eq!(
119 inputs.len(),
120 out.len(),
121 "batch output length must equal input length"
122 );
123 let (inner, outer) = (self.inner.period(), self.outer.period());
124 let n = inputs.len();
125 let span = inner + outer - 1;
126 if !(self.inner.is_fresh() && self.outer.is_fresh())
127 || n < span
128 || !crate::fast::in_range(inputs)
129 {
130 self.batch_nan_into(inputs, out);
131 return;
132 }
133 crate::fast::with_scratch(n, |tmp| {
134 wickra_simd::dispatch(crate::fast::TrimaFast {
135 x: inputs,
136 inner,
137 outer,
138 tmp,
139 out,
140 _borrow: std::marker::PhantomData,
141 });
142 });
143 crate::fast::replay_tail(self, &inputs[n - span..]);
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::traits::BatchExt;
151 use approx::assert_relative_eq;
152
153 #[test]
154 fn new_rejects_zero_period() {
155 assert!(matches!(Trima::new(0), Err(Error::PeriodZero)));
156 }
157
158 #[test]
162 fn accessors_and_metadata() {
163 let mut t = Trima::new(5).unwrap();
164 assert_eq!(t.period(), 5);
165 assert_eq!(t.name(), "TRIMA");
166 assert_eq!(t.value(), None);
167 for i in 1..=t.warmup_period() {
168 t.update(f64::from(u32::try_from(i).unwrap()));
169 }
170 assert!(t.value().is_some());
171 }
172
173 #[test]
174 fn odd_period_reference_values() {
175 let mut trima = Trima::new(5).unwrap();
178 let out = trima.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
179 assert_eq!(out[0], None);
180 assert_eq!(out[3], None);
181 assert_relative_eq!(out[4].unwrap(), 3.0, epsilon = 1e-12);
182 assert_relative_eq!(out[5].unwrap(), 4.0, epsilon = 1e-12);
183 assert_relative_eq!(out[6].unwrap(), 5.0, epsilon = 1e-12);
184 }
185
186 #[test]
187 fn first_emission_at_warmup_period() {
188 let mut trima = Trima::new(6).unwrap();
190 let out = trima.batch(&(1..=10).map(f64::from).collect::<Vec<_>>());
191 assert_eq!(trima.warmup_period(), 6);
192 for v in out.iter().take(5) {
193 assert!(v.is_none());
194 }
195 assert!(out[5].is_some());
196 }
197
198 #[test]
199 fn constant_series_yields_the_constant() {
200 let mut trima = Trima::new(7).unwrap();
201 let out = trima.batch(&[42.0; 20]);
202 for x in out.iter().skip(6) {
203 assert_relative_eq!(x.unwrap(), 42.0, epsilon = 1e-12);
204 }
205 }
206
207 #[test]
208 fn ignores_non_finite_input() {
209 let mut trima = Trima::new(5).unwrap();
210 let ready = trima.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
211 let last = ready[4];
212 assert!(last.is_some());
213 assert_eq!(trima.update(f64::NAN), None);
214 }
215
216 #[test]
217 fn reset_clears_state() {
218 let mut trima = Trima::new(5).unwrap();
219 trima.batch(&(1..=10).map(f64::from).collect::<Vec<_>>());
220 assert!(trima.is_ready());
221 trima.reset();
222 assert!(!trima.is_ready());
223 assert_eq!(trima.update(1.0), None);
224 }
225
226 #[test]
227 fn batch_equals_streaming() {
228 let prices: Vec<f64> = (1..=40).map(f64::from).collect();
229 let batch = Trima::new(8).unwrap().batch(&prices);
230 let mut b = Trima::new(8).unwrap();
231 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
232 assert_eq!(batch, streamed);
233 }
234}