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