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 fn update(&mut self, candle: Candle) -> Option<HeikinAshiOutput> {
68 let ha_close = (candle.open + candle.high + candle.low + candle.close) / 4.0;
69 let ha_open = match self.prev {
70 Some(p) => f64::midpoint(p.open, p.close),
71 None => f64::midpoint(candle.open, candle.close),
73 };
74 let ha_high = candle.high.max(ha_open).max(ha_close);
75 let ha_low = candle.low.min(ha_open).min(ha_close);
76 let out = HeikinAshiOutput {
77 open: ha_open,
78 high: ha_high,
79 low: ha_low,
80 close: ha_close,
81 };
82 self.prev = Some(out);
83 Some(out)
84 }
85
86 fn reset(&mut self) {
87 self.prev = None;
88 }
89
90 fn warmup_period(&self) -> usize {
91 1
92 }
93
94 fn is_ready(&self) -> bool {
95 self.prev.is_some()
96 }
97
98 fn name(&self) -> &'static str {
99 "HeikinAshi"
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::traits::BatchExt;
107 use approx::assert_relative_eq;
108
109 fn cnd(o: f64, h: f64, l: f64, c: f64) -> Candle {
110 Candle::new(o, h, l, c, 0.0, 0).unwrap()
111 }
112
113 #[test]
114 fn first_bar_seeds_open_from_real_open_close() {
115 let mut ha = HeikinAshi::new();
116 let out = ha.update(cnd(10.0, 12.0, 9.0, 11.0)).unwrap();
117 assert_relative_eq!(out.open, (10.0 + 11.0) / 2.0, epsilon = 1e-12);
118 assert_relative_eq!(out.close, (10.0 + 12.0 + 9.0 + 11.0) / 4.0, epsilon = 1e-12);
119 assert!(out.high >= out.open);
121 assert!(out.high >= out.close);
122 assert!(out.low <= out.open);
123 assert!(out.low <= out.close);
124 }
125
126 #[test]
127 fn second_bar_uses_previous_ha_midpoint_as_open() {
128 let mut ha = HeikinAshi::new();
129 let first = ha.update(cnd(10.0, 12.0, 9.0, 11.0)).unwrap();
130 let second = ha.update(cnd(11.5, 13.0, 10.5, 12.0)).unwrap();
131 assert_relative_eq!(
132 second.open,
133 (first.open + first.close) / 2.0,
134 epsilon = 1e-12
135 );
136 assert_relative_eq!(
137 second.close,
138 (11.5 + 13.0 + 10.5 + 12.0) / 4.0,
139 epsilon = 1e-12
140 );
141 }
142
143 #[test]
144 fn batch_equals_streaming() {
145 let candles: Vec<Candle> = (0..50)
146 .map(|i| {
147 let p = 100.0 + f64::from(i);
148 cnd(p, p + 1.5, p - 1.5, p + 0.5)
149 })
150 .collect();
151 let mut a = HeikinAshi::new();
152 let mut b = HeikinAshi::new();
153 let batched = a.batch(&candles);
154 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
155 assert_eq!(batched, streamed);
156 }
157
158 #[test]
159 fn ready_after_first_update() {
160 let mut ha = HeikinAshi::new();
161 assert!(!ha.is_ready());
162 ha.update(cnd(10.0, 11.0, 9.0, 10.5));
163 assert!(ha.is_ready());
164 }
165
166 #[test]
167 fn reset_clears_state() {
168 let mut ha = HeikinAshi::new();
169 ha.update(cnd(10.0, 11.0, 9.0, 10.5));
170 assert!(ha.is_ready());
171 ha.reset();
172 assert!(!ha.is_ready());
173 assert!(ha.value().is_none());
174 let out = ha.update(cnd(20.0, 22.0, 18.0, 21.0)).unwrap();
176 assert_relative_eq!(out.open, (20.0 + 21.0) / 2.0, epsilon = 1e-12);
177 }
178
179 #[test]
180 fn metadata() {
181 let ha = HeikinAshi::new();
182 assert_eq!(ha.warmup_period(), 1);
183 assert_eq!(ha.name(), "HeikinAshi");
184 }
185
186 #[test]
187 fn high_envelopes_open_and_close() {
188 let mut ha = HeikinAshi::new();
190 ha.update(cnd(100.0, 101.0, 99.0, 100.5));
192 let out = ha.update(cnd(50.0, 200.0, 50.0, 200.0)).unwrap();
195 assert_eq!(out.high, 200.0);
196 assert!(out.low <= out.open.min(out.close));
197 }
198}