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 let half = (period / 2).max(1);
40 let smooth = (period as f64).sqrt().round() as usize;
41 let smooth = smooth.max(1);
42 Ok(Self {
43 period,
44 half_wma: Wma::new(half)?,
45 full_wma: Wma::new(period)?,
46 smooth_wma: Wma::new(smooth)?,
47 })
48 }
49
50 pub const fn period(&self) -> usize {
52 self.period
53 }
54}
55
56impl Indicator for Hma {
57 type Input = f64;
58 type Output = f64;
59
60 fn update(&mut self, input: f64) -> Option<f64> {
61 let h = self.half_wma.update(input);
66 let f = self.full_wma.update(input);
67 let (h, f) = (h?, f?);
68 let diff = 2.0 * h - f;
69 self.smooth_wma.update(diff)
70 }
71
72 fn reset(&mut self) {
73 self.half_wma.reset();
74 self.full_wma.reset();
75 self.smooth_wma.reset();
76 }
77
78 fn warmup_period(&self) -> usize {
79 let sm = (self.period as f64).sqrt().round() as usize;
80 self.period + sm.max(1) - 1
81 }
82
83 fn is_ready(&self) -> bool {
84 self.smooth_wma.is_ready()
85 }
86
87 fn name(&self) -> &'static str {
88 "HMA"
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crate::traits::BatchExt;
96 use approx::assert_relative_eq;
97
98 #[test]
99 fn constant_series_yields_constant_hma() {
100 let mut hma = Hma::new(9).unwrap();
101 let out = hma.batch(&[10.0_f64; 80]);
102 let last = out.iter().rev().flatten().next().unwrap();
103 assert_relative_eq!(*last, 10.0, epsilon = 1e-9);
104 }
105
106 #[test]
107 fn batch_equals_streaming() {
108 let prices: Vec<f64> = (1..=100).map(|i| f64::from(i) * 0.7).collect();
109 let mut a = Hma::new(9).unwrap();
110 let mut b = Hma::new(9).unwrap();
111 assert_eq!(
112 a.batch(&prices),
113 prices.iter().map(|p| b.update(*p)).collect::<Vec<_>>()
114 );
115 }
116
117 #[test]
118 fn reset_clears_state() {
119 let mut hma = Hma::new(9).unwrap();
120 hma.batch(&(1..=80).map(f64::from).collect::<Vec<_>>());
121 assert!(hma.is_ready());
122 hma.reset();
123 assert!(!hma.is_ready());
124 }
125
126 #[test]
127 fn rejects_zero_period() {
128 assert!(Hma::new(0).is_err());
129 }
130
131 #[test]
135 fn accessors_and_metadata() {
136 let hma = Hma::new(9).unwrap();
137 assert_eq!(hma.period(), 9);
138 assert_eq!(hma.name(), "HMA");
139 }
140
141 #[test]
142 fn first_emission_matches_warmup_period() {
143 let prices: Vec<f64> = (1..=40).map(f64::from).collect();
144 let mut hma = Hma::new(9).unwrap();
145 let out = hma.batch(&prices);
146 let warmup = hma.warmup_period();
147 assert_eq!(warmup, 11);
148 for (i, v) in out.iter().enumerate().take(warmup - 1) {
149 assert!(v.is_none(), "index {i} must be None during warmup");
150 }
151 assert!(
152 out[warmup - 1].is_some(),
153 "first HMA value must land at warmup_period - 1"
154 );
155 }
156
157 #[test]
158 fn matches_independent_wmas() {
159 let prices: Vec<f64> = (1..=50)
162 .map(|i| (f64::from(i) * 0.3).sin() * 10.0 + 50.0)
163 .collect();
164 let mut hma = Hma::new(9).unwrap();
165 let mut half = Wma::new(4).unwrap(); let mut full = Wma::new(9).unwrap();
167 let mut smooth = Wma::new(3).unwrap(); for (i, &p) in prices.iter().enumerate() {
169 let got = hma.update(p);
170 let want = match (half.update(p), full.update(p)) {
171 (Some(h), Some(f)) => smooth.update(2.0 * h - f),
172 _ => None,
173 };
174 assert_eq!(got.is_some(), want.is_some(), "readiness mismatch at {i}");
176 if let (Some(a), Some(b)) = (got, want) {
177 assert_relative_eq!(a, b, epsilon = 1e-9);
178 }
179 }
180 }
181}