wickra_core/indicators/
hma.rs1use crate::error::{Error, Result};
4use crate::indicators::wma::Wma;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
25pub struct Hma {
26 period: usize,
27 half_wma: Wma,
28 full_wma: Wma,
29 smooth_wma: Wma,
30}
31
32impl Hma {
33 pub fn new(period: usize) -> Result<Self> {
36 if period == 0 {
37 return Err(Error::PeriodZero);
38 }
39 if period > crate::error::MAX_PERIOD {
40 return Err(Error::InvalidPeriod {
41 message: crate::error::PERIOD_ABOVE_MAX,
42 });
43 }
44 let half = (period / 2).max(1);
45 let smooth = (period as f64).sqrt().round() as usize;
46 let smooth = smooth.max(1);
47 Ok(Self {
48 period,
49 half_wma: Wma::new(half)?,
50 full_wma: Wma::new(period)?,
51 smooth_wma: Wma::new(smooth)?,
52 })
53 }
54
55 pub const fn period(&self) -> usize {
57 self.period
58 }
59}
60
61impl Indicator for Hma {
62 type Input = f64;
63 type Output = f64;
64
65 #[inline]
66 fn update(&mut self, input: f64) -> Option<f64> {
67 let h = self.half_wma.update(input);
72 let f = self.full_wma.update(input);
73 let (h, f) = (h?, f?);
74 let diff = 2.0 * h - f;
75 self.smooth_wma.update(diff)
76 }
77
78 fn batch_nan_into(&mut self, inputs: &[f64], out: &mut [f64]) {
85 assert_eq!(
86 inputs.len(),
87 out.len(),
88 "batch output length must equal input length"
89 );
90 let mut start = 0;
91 while !(self.half_wma.is_ready() && self.full_wma.is_ready() && self.smooth_wma.is_ready())
92 && start < inputs.len()
93 {
94 out[start] = self.update(inputs[start]).unwrap_or(f64::NAN);
95 start += 1;
96 }
97 if start == inputs.len() {
98 return;
99 }
100 let (mut half, mut full, mut smooth) = (
101 self.half_wma.steady(),
102 self.full_wma.steady(),
103 self.smooth_wma.steady(),
104 );
105 for (slot, &x) in out[start..].iter_mut().zip(&inputs[start..]) {
106 if !x.is_finite() {
107 *slot = f64::NAN;
108 continue;
109 }
110 let diff = 2.0 * half.step(x) - full.step(x);
111 *slot = if diff.is_finite() {
112 smooth.step(diff)
113 } else {
114 f64::NAN
115 };
116 }
117 }
118
119 fn reset(&mut self) {
120 self.half_wma.reset();
121 self.full_wma.reset();
122 self.smooth_wma.reset();
123 }
124
125 #[inline]
126 fn warmup_period(&self) -> usize {
127 let sm = (self.period as f64).sqrt().round() as usize;
128 self.period + sm.max(1) - 1
129 }
130
131 #[inline]
132 fn is_ready(&self) -> bool {
133 self.smooth_wma.is_ready()
134 }
135
136 #[inline]
137 fn name(&self) -> &'static str {
138 "HMA"
139 }
140
141 fn batch_fast_into(&mut self, inputs: &[f64], out: &mut [f64]) {
148 assert_eq!(
149 inputs.len(),
150 out.len(),
151 "batch output length must equal input length"
152 );
153 let (half, full, smooth) = (
154 self.half_wma.period(),
155 self.full_wma.period(),
156 self.smooth_wma.period(),
157 );
158 let n = inputs.len();
159 let span = full + smooth - 1;
160 if !(self.half_wma.is_empty() && self.full_wma.is_empty() && self.smooth_wma.is_empty())
161 || n < span
162 || !crate::fast::in_range(inputs)
163 {
164 self.batch_nan_into(inputs, out);
165 return;
166 }
167 crate::fast::with_scratch(n, |tmp| {
168 wickra_simd::dispatch(crate::fast::HmaFast {
169 x: inputs,
170 half,
171 full,
172 smooth,
173 tmp,
174 out,
175 _borrow: std::marker::PhantomData,
176 });
177 });
178 crate::fast::replay_tail(self, &inputs[n - span..]);
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use crate::traits::BatchExt;
186 use approx::assert_relative_eq;
187
188 #[test]
189 fn constant_series_yields_constant_hma() {
190 let mut hma = Hma::new(9).unwrap();
191 let out = hma.batch(&[10.0_f64; 80]);
192 let last = out.iter().rev().flatten().next().unwrap();
193 assert_relative_eq!(*last, 10.0, epsilon = 1e-9);
194 }
195
196 #[test]
197 fn batch_equals_streaming() {
198 let prices: Vec<f64> = (1..=100).map(|i| f64::from(i) * 0.7).collect();
199 let mut a = Hma::new(9).unwrap();
200 let mut b = Hma::new(9).unwrap();
201 assert_eq!(
202 a.batch(&prices),
203 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
204 );
205 }
206
207 #[test]
212 fn batch_nan_into_is_the_update_replay_bit_for_bit() {
213 let mut series: Vec<f64> = (0..2_600)
214 .map(|i| 50.0 + (f64::from(i) * 0.11).sin() * 9.0 + f64::from(i % 13))
215 .collect();
216 series[5] = f64::NAN;
217 series[1_030] = f64::INFINITY;
218 series[2_047] = f64::NEG_INFINITY;
219 for x in &mut series[1_500..1_520] {
220 *x = 1.5e308;
221 }
222 let bits = |v: &[f64]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
223 for period in [1, 2, 4, 9, 20] {
224 let mut replay = Hma::new(period).unwrap();
225 let want: Vec<f64> = series
226 .iter()
227 .map(|&x| replay.update(x).unwrap_or(f64::NAN))
228 .collect();
229 for split in [0, 1, 7, 1_023, 1_024, 1_025, 1_900, series.len()] {
230 let mut hma = Hma::new(period).unwrap();
231 let mut got = vec![0.0; series.len()];
232 let (head, tail) = got.split_at_mut(split);
233 hma.batch_nan_into(&series[..split], head);
234 hma.batch_nan_into(&series[split..], tail);
235 assert_eq!(bits(&got), bits(&want), "period {period} split {split}");
236 assert_eq!(hma.update(55.0), replay.clone().update(55.0));
237 }
238 }
239 }
240
241 #[test]
242 fn reset_clears_state() {
243 let mut hma = Hma::new(9).unwrap();
244 hma.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
245 assert!(hma.is_ready());
246 hma.reset();
247 assert!(!hma.is_ready());
248 }
249
250 #[test]
251 fn rejects_zero_period() {
252 assert!(Hma::new(0).is_err());
253 }
254
255 #[test]
259 fn accessors_and_metadata() {
260 let hma = Hma::new(9).unwrap();
261 assert_eq!(hma.period(), 9);
262 assert_eq!(hma.name(), "HMA");
263 }
264
265 #[test]
266 fn first_emission_matches_warmup_period() {
267 let prices: Vec<f64> = (1..=40).map(f64::from).collect();
268 let mut hma = Hma::new(9).unwrap();
269 let out = hma.batch(&prices);
270 let warmup = hma.warmup_period();
271 assert_eq!(warmup, 11);
272 for (i, v) in out.iter().enumerate().take(warmup - 1) {
273 assert!(v.is_none(), "index {i} must be None during warmup");
274 }
275 assert!(
276 out[warmup - 1].is_some(),
277 "first HMA value must land at warmup_period - 1"
278 );
279 }
280
281 #[test]
282 fn matches_independent_wmas() {
283 let prices: Vec<f64> = (1..=50)
286 .map(|i| (f64::from(i) * 0.3).sin() * 10.0 + 50.0)
287 .collect();
288 let mut hma = Hma::new(9).unwrap();
289 let mut half = Wma::new(4).unwrap(); let mut full = Wma::new(9).unwrap();
291 let mut smooth = Wma::new(3).unwrap(); for (i, &p) in prices.iter().enumerate() {
293 let got = hma.update(p);
294 let want = match (half.update(p), full.update(p)) {
295 (Some(h), Some(f)) => smooth.update(2.0 * h - f),
296 _ => None,
297 };
298 assert_eq!(got.is_some(), want.is_some(), "readiness mismatch at {i}");
300 if let (Some(a), Some(b)) = (got, want) {
301 assert_relative_eq!(a, b, epsilon = 1e-9);
302 }
303 }
304 }
305}