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
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::traits::BatchExt;
117 use approx::assert_relative_eq;
118
119 #[test]
120 fn new_rejects_zero_period() {
121 assert!(matches!(Trima::new(0), Err(Error::PeriodZero)));
122 }
123
124 #[test]
128 fn accessors_and_metadata() {
129 let mut t = Trima::new(5).unwrap();
130 assert_eq!(t.period(), 5);
131 assert_eq!(t.name(), "TRIMA");
132 assert_eq!(t.value(), None);
133 for i in 1..=t.warmup_period() {
134 t.update(f64::from(u32::try_from(i).unwrap()));
135 }
136 assert!(t.value().is_some());
137 }
138
139 #[test]
140 fn odd_period_reference_values() {
141 let mut trima = Trima::new(5).unwrap();
144 let out = trima.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
145 assert_eq!(out[0], None);
146 assert_eq!(out[3], None);
147 assert_relative_eq!(out[4].unwrap(), 3.0, epsilon = 1e-12);
148 assert_relative_eq!(out[5].unwrap(), 4.0, epsilon = 1e-12);
149 assert_relative_eq!(out[6].unwrap(), 5.0, epsilon = 1e-12);
150 }
151
152 #[test]
153 fn first_emission_at_warmup_period() {
154 let mut trima = Trima::new(6).unwrap();
156 let out = trima.batch(&(1..=10).map(f64::from).collect::<Vec<_>>());
157 assert_eq!(trima.warmup_period(), 6);
158 for v in out.iter().take(5) {
159 assert!(v.is_none());
160 }
161 assert!(out[5].is_some());
162 }
163
164 #[test]
165 fn constant_series_yields_the_constant() {
166 let mut trima = Trima::new(7).unwrap();
167 let out = trima.batch(&[42.0; 20]);
168 for x in out.iter().skip(6) {
169 assert_relative_eq!(x.unwrap(), 42.0, epsilon = 1e-12);
170 }
171 }
172
173 #[test]
174 fn ignores_non_finite_input() {
175 let mut trima = Trima::new(5).unwrap();
176 let ready = trima.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
177 let last = ready[4];
178 assert!(last.is_some());
179 assert_eq!(trima.update(f64::NAN), None);
180 }
181
182 #[test]
183 fn reset_clears_state() {
184 let mut trima = Trima::new(5).unwrap();
185 trima.batch(&(1..=10).map(f64::from).collect::<Vec<_>>());
186 assert!(trima.is_ready());
187 trima.reset();
188 assert!(!trima.is_ready());
189 assert_eq!(trima.update(1.0), None);
190 }
191
192 #[test]
193 fn batch_equals_streaming() {
194 let prices: Vec<f64> = (1..=40).map(f64::from).collect();
195 let batch = Trima::new(8).unwrap().batch(&prices);
196 let mut b = Trima::new(8).unwrap();
197 let streamed: Vec<_> = prices.iter().map(|p| b.update(*p)).collect();
198 assert_eq!(batch, streamed);
199 }
200}