Skip to main content

quantwave_core/indicators/
hma.rs

1use 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        // HMA is complex to verify manually, but we can check if it returns values
97        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};