use crate::indicators::metadata::IndicatorMetadata;
use crate::traits::Next;
#[derive(Debug, Clone)]
pub struct HeikinAshi {
prev_ha_open: Option<f64>,
prev_ha_close: Option<f64>,
}
impl HeikinAshi {
pub fn new() -> Self {
Self {
prev_ha_open: None,
prev_ha_close: None,
}
}
}
impl Next<(f64, f64, f64, f64)> for HeikinAshi {
type Output = (f64, f64, f64, f64);
fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
let ha_close = (open + high + low + close) / 4.0;
let ha_open = match (self.prev_ha_open, self.prev_ha_close) {
(Some(prev_open), Some(prev_close)) => (prev_open + prev_close) / 2.0,
_ => (open + close) / 2.0,
};
let ha_high = high.max(ha_open).max(ha_close);
let ha_low = low.min(ha_open).min(ha_close);
self.prev_ha_open = Some(ha_open);
self.prev_ha_close = Some(ha_close);
(ha_open, ha_high, ha_low, ha_close)
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use serde::Deserialize;
use std::fs;
use std::path::Path;
#[derive(Debug, Deserialize)]
struct HACase {
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
expected_open: Vec<f64>,
expected_high: Vec<f64>,
expected_low: Vec<f64>,
expected_close: Vec<f64>,
}
#[test]
fn test_heikin_ashi_gold_standard() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_path = Path::new(&manifest_dir);
let path = manifest_path.join("tests/gold_standard/heikin_ashi.json");
let path = if path.exists() {
path
} else {
manifest_path
.parent()
.unwrap()
.join("tests/gold_standard/heikin_ashi.json")
};
let content = fs::read_to_string(path).unwrap();
let case: HACase = serde_json::from_str(&content).unwrap();
let mut ha = HeikinAshi::new();
for i in 0..case.open.len() {
let (o, h, l, c) = ha.next((case.open[i], case.high[i], case.low[i], case.close[i]));
approx::assert_relative_eq!(o, case.expected_open[i], epsilon = 1e-6);
approx::assert_relative_eq!(h, case.expected_high[i], epsilon = 1e-6);
approx::assert_relative_eq!(l, case.expected_low[i], epsilon = 1e-6);
approx::assert_relative_eq!(c, case.expected_close[i], epsilon = 1e-6);
}
}
fn heikin_ashi_batch(data: Vec<(f64, f64, f64, f64)>) -> Vec<(f64, f64, f64, f64)> {
let mut ha = HeikinAshi::new();
data.into_iter().map(|x| ha.next(x)).collect()
}
proptest! {
#[test]
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)) {
let mut adj_input = Vec::with_capacity(input.len());
for (o, h, l, c) in input {
let o_f: f64 = o;
let h_f: f64 = h;
let l_f: f64 = l;
let c_f: f64 = c;
let high = h_f.max(o_f).max(l_f).max(c_f);
let low = l_f.min(o_f).min(h_f).min(c_f);
adj_input.push((o_f, high, low, c_f));
}
let mut ha = HeikinAshi::new();
let mut streaming_results = Vec::with_capacity(adj_input.len());
for &val in &adj_input {
streaming_results.push(ha.next(val));
}
let batch_results = heikin_ashi_batch(adj_input);
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
approx::assert_relative_eq!(s.2, b.2, epsilon = 1e-6);
approx::assert_relative_eq!(s.3, b.3, epsilon = 1e-6);
}
}
}
#[test]
fn test_heikin_ashi_basic() {
let mut ha = HeikinAshi::new();
let (o1, h1, l1, c1) = ha.next((10.0, 12.0, 8.0, 11.0));
assert_eq!(o1, 10.5);
assert_eq!(h1, 12.0);
assert_eq!(l1, 8.0);
assert_eq!(c1, 10.25);
let (o2, h2, l2, c2) = ha.next((11.0, 13.0, 10.0, 12.0));
assert_eq!(o2, 10.375);
assert_eq!(h2, 13.0);
assert_eq!(l2, 10.0);
assert_eq!(c2, 11.5);
}
}
pub const HEIKIN_ASHI_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Heikin-Ashi",
description: "Heikin-Ashi candles filter market noise to reveal the underlying trend.",
params: &[],
formula_source: "https://www.investopedia.com/trading/heikin-ashi-better-candlestick/",
formula_latex: r#"
\[
HA_{Close} = \frac{O + H + L + C}{4} \\ HA_{Open} = \frac{HA_{Open, t-1} + HA_{Close, t-1}}{2}
\]
"#,
gold_standard_file: "heikin_ashi.json",
category: "Classic",
};