quantwave_core/indicators/
heikin_ashi.rs1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3
4#[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); 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 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 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};