Skip to main content

quantwave_core/indicators/
heikin_ashi.rs

1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3
4/// Heikin-Ashi Candlesticks
5/// HA_Close = (Open + High + Low + Close) / 4
6/// HA_Open = (prev_HA_Open + prev_HA_Close) / 2
7/// HA_High = max(High, HA_Open, HA_Close)
8/// HA_Low = min(Low, HA_Open, HA_Close)
9#[derive(Debug, Clone)]
10pub struct HeikinAshi {
11    prev_ha_open: Option<f64>,
12    prev_ha_close: Option<f64>,
13}
14
15impl HeikinAshi {
16    pub fn new() -> Self {
17        Self {
18            prev_ha_open: None,
19            prev_ha_close: None,
20        }
21    }
22}
23
24impl Next<(f64, f64, f64, f64)> for HeikinAshi {
25    type Output = (f64, f64, f64, f64); // (HA_Open, HA_High, HA_Low, HA_Close)
26
27    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
28        let ha_close = (open + high + low + close) / 4.0;
29
30        let ha_open = match (self.prev_ha_open, self.prev_ha_close) {
31            (Some(prev_open), Some(prev_close)) => (prev_open + prev_close) / 2.0,
32            _ => (open + close) / 2.0,
33        };
34
35        let ha_high = high.max(ha_open).max(ha_close);
36        let ha_low = low.min(ha_open).min(ha_close);
37
38        self.prev_ha_open = Some(ha_open);
39        self.prev_ha_close = Some(ha_close);
40
41        (ha_open, ha_high, ha_low, ha_close)
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use proptest::prelude::*;
49    use serde::Deserialize;
50    use std::fs;
51    use std::path::Path;
52
53    #[derive(Debug, Deserialize)]
54    struct HACase {
55        open: Vec<f64>,
56        high: Vec<f64>,
57        low: Vec<f64>,
58        close: Vec<f64>,
59        expected_open: Vec<f64>,
60        expected_high: Vec<f64>,
61        expected_low: Vec<f64>,
62        expected_close: Vec<f64>,
63    }
64
65    #[test]
66    fn test_heikin_ashi_gold_standard() {
67        let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
68        let manifest_path = Path::new(&manifest_dir);
69        let path = manifest_path.join("tests/gold_standard/heikin_ashi.json");
70        let path = if path.exists() {
71            path
72        } else {
73            manifest_path
74                .parent()
75                .unwrap()
76                .join("tests/gold_standard/heikin_ashi.json")
77        };
78        let content = fs::read_to_string(path).unwrap();
79        let case: HACase = serde_json::from_str(&content).unwrap();
80
81        let mut ha = HeikinAshi::new();
82        for i in 0..case.open.len() {
83            let (o, h, l, c) = ha.next((case.open[i], case.high[i], case.low[i], case.close[i]));
84            approx::assert_relative_eq!(o, case.expected_open[i], epsilon = 1e-6);
85            approx::assert_relative_eq!(h, case.expected_high[i], epsilon = 1e-6);
86            approx::assert_relative_eq!(l, case.expected_low[i], epsilon = 1e-6);
87            approx::assert_relative_eq!(c, case.expected_close[i], epsilon = 1e-6);
88        }
89    }
90
91    fn heikin_ashi_batch(data: Vec<(f64, f64, f64, f64)>) -> Vec<(f64, f64, f64, f64)> {
92        let mut ha = HeikinAshi::new();
93        data.into_iter().map(|x| ha.next(x)).collect()
94    }
95
96    proptest! {
97        #[test]
98        fn test_heikin_ashi_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0, 0.0..100.0, 0.0..100.0), 1..100)) {
99            let mut adj_input = Vec::with_capacity(input.len());
100            for (o, h, l, c) in input {
101                let o_f: f64 = o;
102                let h_f: f64 = h;
103                let l_f: f64 = l;
104                let c_f: f64 = c;
105                let high = h_f.max(o_f).max(l_f).max(c_f);
106                let low = l_f.min(o_f).min(h_f).min(c_f);
107                adj_input.push((o_f, high, low, c_f));
108            }
109
110            let mut ha = HeikinAshi::new();
111            let mut streaming_results = Vec::with_capacity(adj_input.len());
112            for &val in &adj_input {
113                streaming_results.push(ha.next(val));
114            }
115
116            let batch_results = heikin_ashi_batch(adj_input);
117
118            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
119                approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
120                approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
121                approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-6);
122                approx::assert_relative_eq!(s.3, b.3, epsilon = 1e-6);
123            }
124        }
125    }
126
127    #[test]
128    fn test_heikin_ashi_basic() {
129        let mut ha = HeikinAshi::new();
130
131        // Bar 1: O=10, H=12, L=8, C=11
132        // HA_Close = (10+12+8+11)/4 = 41/4 = 10.25
133        // HA_Open = (10+11)/2 = 10.5
134        // HA_High = max(12, 10.5, 10.25) = 12
135        // HA_Low = min(8, 10.5, 10.25) = 8
136        let (o1, h1, l1, c1) = ha.next((10.0, 12.0, 8.0, 11.0));
137        assert_eq!(o1, 10.5);
138        assert_eq!(h1, 12.0);
139        assert_eq!(l1, 8.0);
140        assert_eq!(c1, 10.25);
141
142        // Bar 2: O=11, H=13, L=10, C=12
143        // HA_Close = (11+13+10+12)/4 = 46/4 = 11.5
144        // HA_Open = (10.5 + 10.25)/2 = 20.75 / 2 = 10.375
145        // HA_High = max(13, 10.375, 11.5) = 13
146        // HA_Low = min(10, 10.375, 11.5) = 10
147        let (o2, h2, l2, c2) = ha.next((11.0, 13.0, 10.0, 12.0));
148        assert_eq!(o2, 10.375);
149        assert_eq!(h2, 13.0);
150        assert_eq!(l2, 10.0);
151        assert_eq!(c2, 11.5);
152    }
153}
154
155pub const HEIKIN_ASHI_METADATA: IndicatorMetadata = IndicatorMetadata {
156    name: "Heikin-Ashi",
157    description: "Heikin-Ashi candles filter market noise to reveal the underlying trend.",
158    params: &[],
159    formula_source: "https://www.investopedia.com/trading/heikin-ashi-better-candlestick/",
160    formula_latex: r#"
161\[
162HA_{Close} = \frac{O + H + L + C}{4} \\ HA_{Open} = \frac{HA_{Open, t-1} + HA_{Close, t-1}}{2}
163\]
164"#,
165    gold_standard_file: "heikin_ashi.json",
166    category: "Classic",
167};