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 reset(&mut self) {
79 self.half_wma.reset();
80 self.full_wma.reset();
81 self.smooth_wma.reset();
82 }
83
84 #[inline]
85 fn warmup_period(&self) -> usize {
86 let sm = (self.period as f64).sqrt().round() as usize;
87 self.period + sm.max(1) - 1
88 }
89
90 #[inline]
91 fn is_ready(&self) -> bool {
92 self.smooth_wma.is_ready()
93 }
94
95 #[inline]
96 fn name(&self) -> &'static str {
97 "HMA"
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104 use crate::traits::BatchExt;
105 use approx::assert_relative_eq;
106
107 #[test]
108 fn constant_series_yields_constant_hma() {
109 let mut hma = Hma::new(9).unwrap();
110 let out = hma.batch(&[10.0_f64; 80]);
111 let last = out.iter().rev().flatten().next().unwrap();
112 assert_relative_eq!(*last, 10.0, epsilon = 1e-9);
113 }
114
115 #[test]
116 fn batch_equals_streaming() {
117 let prices: Vec<f64> = (1..=100).map(|i| f64::from(i) * 0.7).collect();
118 let mut a = Hma::new(9).unwrap();
119 let mut b = Hma::new(9).unwrap();
120 assert_eq!(
121 a.batch(&prices),
122 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
123 );
124 }
125
126 #[test]
127 fn reset_clears_state() {
128 let mut hma = Hma::new(9).unwrap();
129 hma.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
130 assert!(hma.is_ready());
131 hma.reset();
132 assert!(!hma.is_ready());
133 }
134
135 #[test]
136 fn rejects_zero_period() {
137 assert!(Hma::new(0).is_err());
138 }
139
140 #[test]
144 fn accessors_and_metadata() {
145 let hma = Hma::new(9).unwrap();
146 assert_eq!(hma.period(), 9);
147 assert_eq!(hma.name(), "HMA");
148 }
149
150 #[test]
151 fn first_emission_matches_warmup_period() {
152 let prices: Vec<f64> = (1..=40).map(f64::from).collect();
153 let mut hma = Hma::new(9).unwrap();
154 let out = hma.batch(&prices);
155 let warmup = hma.warmup_period();
156 assert_eq!(warmup, 11);
157 for (i, v) in out.iter().enumerate().take(warmup - 1) {
158 assert!(v.is_none(), "index {i} must be None during warmup");
159 }
160 assert!(
161 out[warmup - 1].is_some(),
162 "first HMA value must land at warmup_period - 1"
163 );
164 }
165
166 #[test]
167 fn matches_independent_wmas() {
168 let prices: Vec<f64> = (1..=50)
171 .map(|i| (f64::from(i) * 0.3).sin() * 10.0 + 50.0)
172 .collect();
173 let mut hma = Hma::new(9).unwrap();
174 let mut half = Wma::new(4).unwrap(); let mut full = Wma::new(9).unwrap();
176 let mut smooth = Wma::new(3).unwrap(); for (i, &p) in prices.iter().enumerate() {
178 let got = hma.update(p);
179 let want = match (half.update(p), full.update(p)) {
180 (Some(h), Some(f)) => smooth.update(2.0 * h - f),
181 _ => None,
182 };
183 assert_eq!(got.is_some(), want.is_some(), "readiness mismatch at {i}");
185 if let (Some(a), Some(b)) = (got, want) {
186 assert_relative_eq!(a, b, epsilon = 1e-9);
187 }
188 }
189 }
190}