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 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); 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 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 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};