#![allow(clippy::manual_midpoint)]
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct HeikinAshiOutput {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
}
#[derive(Debug, Clone, Default)]
pub struct HeikinAshi {
prev: Option<HeikinAshiOutput>,
}
impl HeikinAshi {
#[must_use]
pub const fn new() -> Self {
Self { prev: None }
}
pub const fn value(&self) -> Option<HeikinAshiOutput> {
self.prev
}
}
impl Indicator for HeikinAshi {
type Input = Candle;
type Output = HeikinAshiOutput;
fn update(&mut self, candle: Candle) -> Option<HeikinAshiOutput> {
let ha_close = (candle.open + candle.high + candle.low + candle.close) / 4.0;
let ha_open = match self.prev {
Some(p) => f64::midpoint(p.open, p.close),
None => f64::midpoint(candle.open, candle.close),
};
let ha_high = candle.high.max(ha_open).max(ha_close);
let ha_low = candle.low.min(ha_open).min(ha_close);
let out = HeikinAshiOutput {
open: ha_open,
high: ha_high,
low: ha_low,
close: ha_close,
};
self.prev = Some(out);
Some(out)
}
fn reset(&mut self) {
self.prev = None;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.prev.is_some()
}
fn name(&self) -> &'static str {
"HeikinAshi"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn cnd(o: f64, h: f64, l: f64, c: f64) -> Candle {
Candle::new(o, h, l, c, 0.0, 0).unwrap()
}
#[test]
fn first_bar_seeds_open_from_real_open_close() {
let mut ha = HeikinAshi::new();
let out = ha.update(cnd(10.0, 12.0, 9.0, 11.0)).unwrap();
assert_relative_eq!(out.open, (10.0 + 11.0) / 2.0, epsilon = 1e-12);
assert_relative_eq!(out.close, (10.0 + 12.0 + 9.0 + 11.0) / 4.0, epsilon = 1e-12);
assert!(out.high >= out.open);
assert!(out.high >= out.close);
assert!(out.low <= out.open);
assert!(out.low <= out.close);
}
#[test]
fn second_bar_uses_previous_ha_midpoint_as_open() {
let mut ha = HeikinAshi::new();
let first = ha.update(cnd(10.0, 12.0, 9.0, 11.0)).unwrap();
let second = ha.update(cnd(11.5, 13.0, 10.5, 12.0)).unwrap();
assert_relative_eq!(
second.open,
(first.open + first.close) / 2.0,
epsilon = 1e-12
);
assert_relative_eq!(
second.close,
(11.5 + 13.0 + 10.5 + 12.0) / 4.0,
epsilon = 1e-12
);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..50)
.map(|i| {
let p = 100.0 + f64::from(i);
cnd(p, p + 1.5, p - 1.5, p + 0.5)
})
.collect();
let mut a = HeikinAshi::new();
let mut b = HeikinAshi::new();
let batched = a.batch(&candles);
let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
assert_eq!(batched, streamed);
}
#[test]
fn ready_after_first_update() {
let mut ha = HeikinAshi::new();
assert!(!ha.is_ready());
ha.update(cnd(10.0, 11.0, 9.0, 10.5));
assert!(ha.is_ready());
}
#[test]
fn reset_clears_state() {
let mut ha = HeikinAshi::new();
ha.update(cnd(10.0, 11.0, 9.0, 10.5));
assert!(ha.is_ready());
ha.reset();
assert!(!ha.is_ready());
assert!(ha.value().is_none());
let out = ha.update(cnd(20.0, 22.0, 18.0, 21.0)).unwrap();
assert_relative_eq!(out.open, (20.0 + 21.0) / 2.0, epsilon = 1e-12);
}
#[test]
fn metadata() {
let ha = HeikinAshi::new();
assert_eq!(ha.warmup_period(), 1);
assert_eq!(ha.name(), "HeikinAshi");
}
#[test]
fn high_envelopes_open_and_close() {
let mut ha = HeikinAshi::new();
ha.update(cnd(100.0, 101.0, 99.0, 100.5));
let out = ha.update(cnd(50.0, 200.0, 50.0, 200.0)).unwrap();
assert_eq!(out.high, 200.0);
assert!(out.low <= out.open.min(out.close));
}
}