wickra_core/indicators/
heikin_ashi.rs1#![allow(clippy::manual_midpoint)]
3use crate::ohlcv::Candle;
10use crate::traits::Indicator;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct HeikinAshiOutput {
18 pub open: f64,
20 pub high: f64,
22 pub low: f64,
24 pub close: f64,
26}
27
28#[derive(Debug, Clone, Default)]
46pub struct HeikinAshi {
47 prev: Option<HeikinAshiOutput>,
48}
49
50impl HeikinAshi {
51 #[must_use]
53 pub const fn new() -> Self {
54 Self { prev: None }
55 }
56
57 pub const fn value(&self) -> Option<HeikinAshiOutput> {
59 self.prev
60 }
61}
62
63impl Indicator for HeikinAshi {
64 type Input = Candle;
65 type Output = HeikinAshiOutput;
66
67 #[inline]
68 fn update(&mut self, candle: Candle) -> Option<HeikinAshiOutput> {
69 let ha_close = (candle.open + candle.high + candle.low + candle.close) / 4.0;
70 let ha_open = match self.prev {
71 Some(p) => f64::midpoint(p.open, p.close),
72 None => f64::midpoint(candle.open, candle.close),
74 };
75 let ha_high = candle.high.max(ha_open).max(ha_close);
76 let ha_low = candle.low.min(ha_open).min(ha_close);
77 let out = HeikinAshiOutput {
78 open: ha_open,
79 high: ha_high,
80 low: ha_low,
81 close: ha_close,
82 };
83 self.prev = Some(out);
84 Some(out)
85 }
86
87 fn reset(&mut self) {
88 self.prev = None;
89 }
90
91 #[inline]
92 fn warmup_period(&self) -> usize {
93 1
94 }
95
96 #[inline]
97 fn is_ready(&self) -> bool {
98 self.prev.is_some()
99 }
100
101 #[inline]
102 fn name(&self) -> &'static str {
103 "HeikinAshi"
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::traits::BatchExt;
111 use approx::assert_relative_eq;
112
113 fn cnd(o: f64, h: f64, l: f64, c: f64) -> Candle {
114 Candle::new(o, h, l, c, 0.0, 0).unwrap()
115 }
116
117 #[test]
118 fn first_bar_seeds_open_from_real_open_close() {
119 let mut ha = HeikinAshi::new();
120 let out = ha.update(cnd(10.0, 12.0, 9.0, 11.0)).unwrap();
121 assert_relative_eq!(out.open, (10.0 + 11.0) / 2.0, epsilon = 1e-12);
122 assert_relative_eq!(out.close, (10.0 + 12.0 + 9.0 + 11.0) / 4.0, epsilon = 1e-12);
123 assert!(out.high >= out.open);
125 assert!(out.high >= out.close);
126 assert!(out.low <= out.open);
127 assert!(out.low <= out.close);
128 }
129
130 #[test]
131 fn second_bar_uses_previous_ha_midpoint_as_open() {
132 let mut ha = HeikinAshi::new();
133 let first = ha.update(cnd(10.0, 12.0, 9.0, 11.0)).unwrap();
134 let second = ha.update(cnd(11.5, 13.0, 10.5, 12.0)).unwrap();
135 assert_relative_eq!(
136 second.open,
137 (first.open + first.close) / 2.0,
138 epsilon = 1e-12
139 );
140 assert_relative_eq!(
141 second.close,
142 (11.5 + 13.0 + 10.5 + 12.0) / 4.0,
143 epsilon = 1e-12
144 );
145 }
146
147 #[test]
148 fn batch_equals_streaming() {
149 let candles: Vec<Candle> = (0..50)
150 .map(|i| {
151 let p = 100.0 + f64::from(i);
152 cnd(p, p + 1.5, p - 1.5, p + 0.5)
153 })
154 .collect();
155 let mut a = HeikinAshi::new();
156 let mut b = HeikinAshi::new();
157 let batched = a.batch(&candles);
158 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
159 assert_eq!(batched, streamed);
160 }
161
162 #[test]
163 fn ready_after_first_update() {
164 let mut ha = HeikinAshi::new();
165 assert!(!ha.is_ready());
166 ha.update(cnd(10.0, 11.0, 9.0, 10.5));
167 assert!(ha.is_ready());
168 }
169
170 #[test]
171 fn reset_clears_state() {
172 let mut ha = HeikinAshi::new();
173 ha.update(cnd(10.0, 11.0, 9.0, 10.5));
174 assert!(ha.is_ready());
175 ha.reset();
176 assert!(!ha.is_ready());
177 assert!(ha.value().is_none());
178 let out = ha.update(cnd(20.0, 22.0, 18.0, 21.0)).unwrap();
180 assert_relative_eq!(out.open, (20.0 + 21.0) / 2.0, epsilon = 1e-12);
181 }
182
183 #[test]
184 fn metadata() {
185 let ha = HeikinAshi::new();
186 assert_eq!(ha.warmup_period(), 1);
187 assert_eq!(ha.name(), "HeikinAshi");
188 }
189
190 #[test]
191 fn high_envelopes_open_and_close() {
192 let mut ha = HeikinAshi::new();
194 ha.update(cnd(100.0, 101.0, 99.0, 100.5));
196 let out = ha.update(cnd(50.0, 200.0, 50.0, 200.0)).unwrap();
199 assert_eq!(out.high, 200.0);
200 assert!(out.low <= out.open.min(out.close));
201 }
202}