quantwave_core/indicators/
hma.rs1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::indicators::smoothing::WMA;
3use crate::traits::Next;
4
5#[derive(Debug, Clone)]
6pub struct HMA {
7 wma_half: WMA,
8 wma_full: WMA,
9 wma_sqrt: WMA,
10}
11
12impl HMA {
13 pub fn new(period: usize) -> Self {
14 Self {
15 wma_half: WMA::new(period / 2),
16 wma_full: WMA::new(period),
17 wma_sqrt: WMA::new((period as f64).sqrt() as usize),
18 }
19 }
20}
21
22impl Next<f64> for HMA {
23 type Output = f64;
24
25 fn next(&mut self, input: f64) -> Self::Output {
26 let wma_half = self.wma_half.next(input);
27 let wma_full = self.wma_full.next(input);
28 let raw = 2.0 * wma_half - wma_full;
29 self.wma_sqrt.next(raw)
30 }
31}
32
33#[cfg(test)]
34mod tests {
35 use super::*;
36 use proptest::prelude::*;
37 use serde::Deserialize;
38 use std::fs;
39 use std::path::Path;
40
41 #[derive(Debug, Deserialize)]
42 struct HMACase {
43 close: Vec<f64>,
44 expected_hma: Vec<f64>,
45 }
46
47 #[test]
48 fn test_hma_gold_standard() {
49 let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
50 let manifest_path = Path::new(&manifest_dir);
51 let path = manifest_path.join("tests/gold_standard/hma_14.json");
52 let path = if path.exists() {
53 path
54 } else {
55 manifest_path
56 .parent()
57 .unwrap()
58 .join("tests/gold_standard/hma_14.json")
59 };
60 let content = fs::read_to_string(path).unwrap();
61 let case: HMACase = serde_json::from_str(&content).unwrap();
62
63 let mut hma = HMA::new(14);
64 for i in 0..case.close.len() {
65 let res = hma.next(case.close[i]);
66 approx::assert_relative_eq!(res, case.expected_hma[i], epsilon = 1e-6);
67 }
68 }
69
70 fn hma_batch(data: Vec<f64>, period: usize) -> Vec<f64> {
71 let mut hma = HMA::new(period);
72 data.into_iter().map(|x| hma.next(x)).collect()
73 }
74
75 proptest! {
76 #[test]
77 fn test_hma_parity(input in prop::collection::vec(0.0..1000.0, 1..100)) {
78 let period = 14;
79 let mut hma = HMA::new(period);
80 let mut streaming_results = Vec::with_capacity(input.len());
81 for &val in &input {
82 streaming_results.push(hma.next(val));
83 }
84
85 let batch_results = hma_batch(input, period);
86
87 for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
88 approx::assert_relative_eq!(s, b, epsilon = 1e-6);
89 }
90 }
91 }
92
93 #[test]
94 fn test_hma_basic() {
95 let mut hma = HMA::new(20);
96 for i in 0..100 {
98 let val = hma.next(i as f64);
99 if i > 20 {
100 assert!(val > 0.0);
101 }
102 }
103 }
104}
105
106pub const HMA_METADATA: IndicatorMetadata = IndicatorMetadata {
107 name: "Hull Moving Average",
108 description: "The Hull Moving Average (HMA) aims to reduce lag while maintaining smoothness.",
109 params: &[ParamDef {
110 name: "period",
111 default: "14",
112 description: "Smoothing period",
113 }],
114 formula_source: "https://alanhull.com/hull-moving-average",
115 formula_latex: r#"
116\[
117HMA = WMA(2 \times WMA(\frac{n}{2}) - WMA(n), \sqrt{n})
118\]
119"#,
120 gold_standard_file: "hma.json",
121 category: "Classic",
122};