wickra_core/indicators/
smoothed_heikin_ashi.rs1use crate::error::{Error, Result};
4use crate::indicators::ema::Ema;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10pub struct SmoothedHeikinAshiOutput {
11 pub open: f64,
13 pub high: f64,
15 pub low: f64,
17 pub close: f64,
19}
20
21#[derive(Debug, Clone)]
56pub struct SmoothedHeikinAshi {
57 period: usize,
58 ema_open: Ema,
59 ema_high: Ema,
60 ema_low: Ema,
61 ema_close: Ema,
62 prev: Option<SmoothedHeikinAshiOutput>,
63 last: Option<SmoothedHeikinAshiOutput>,
64}
65
66impl SmoothedHeikinAshi {
67 pub fn new(period: usize) -> Result<Self> {
73 if period == 0 {
74 return Err(Error::PeriodZero);
75 }
76 Ok(Self {
77 period,
78 ema_open: Ema::new(period)?,
79 ema_high: Ema::new(period)?,
80 ema_low: Ema::new(period)?,
81 ema_close: Ema::new(period)?,
82 prev: None,
83 last: None,
84 })
85 }
86
87 pub const fn period(&self) -> usize {
89 self.period
90 }
91
92 pub const fn value(&self) -> Option<SmoothedHeikinAshiOutput> {
94 self.last
95 }
96}
97
98impl Indicator for SmoothedHeikinAshi {
99 type Input = Candle;
100 type Output = SmoothedHeikinAshiOutput;
101
102 fn update(&mut self, candle: Candle) -> Option<SmoothedHeikinAshiOutput> {
103 let eo = self.ema_open.update(candle.open);
104 let eh = self.ema_high.update(candle.high);
105 let el = self.ema_low.update(candle.low);
106 let ec = self.ema_close.update(candle.close);
107 let (Some(eo), Some(eh), Some(el), Some(ec)) = (eo, eh, el, ec) else {
108 return None;
109 };
110 let ha_close = (eo + eh + el + ec) / 4.0;
111 let ha_open = match self.prev {
112 Some(p) => f64::midpoint(p.open, p.close),
113 None => f64::midpoint(eo, ec),
114 };
115 let ha_high = eh.max(ha_open).max(ha_close);
116 let ha_low = el.min(ha_open).min(ha_close);
117 let out = SmoothedHeikinAshiOutput {
118 open: ha_open,
119 high: ha_high,
120 low: ha_low,
121 close: ha_close,
122 };
123 self.prev = Some(out);
124 self.last = Some(out);
125 Some(out)
126 }
127
128 fn reset(&mut self) {
129 self.ema_open.reset();
130 self.ema_high.reset();
131 self.ema_low.reset();
132 self.ema_close.reset();
133 self.prev = None;
134 self.last = None;
135 }
136
137 fn warmup_period(&self) -> usize {
138 self.period
139 }
140
141 fn is_ready(&self) -> bool {
142 self.last.is_some()
143 }
144
145 fn name(&self) -> &'static str {
146 "SmoothedHeikinAshi"
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::traits::BatchExt;
154
155 fn c(open: f64, high: f64, low: f64, close: f64) -> Candle {
156 Candle::new_unchecked(open, high, low, close, 1_000.0, 0)
157 }
158
159 #[test]
160 fn rejects_zero_period() {
161 assert!(matches!(SmoothedHeikinAshi::new(0), Err(Error::PeriodZero)));
162 }
163
164 #[test]
165 fn accessors_and_metadata() {
166 let s = SmoothedHeikinAshi::new(10).unwrap();
167 assert_eq!(s.period(), 10);
168 assert_eq!(s.warmup_period(), 10);
169 assert_eq!(s.name(), "SmoothedHeikinAshi");
170 assert!(!s.is_ready());
171 assert_eq!(s.value(), None);
172 }
173
174 #[test]
175 fn first_emission_at_warmup_period() {
176 let mut s = SmoothedHeikinAshi::new(3).unwrap();
177 let candles: Vec<Candle> = (0..6)
178 .map(|i| {
179 let b = 100.0 + f64::from(i);
180 c(b, b + 1.0, b - 1.0, b + 0.5)
181 })
182 .collect();
183 let out = s.batch(&candles);
184 for v in out.iter().take(2) {
185 assert!(v.is_none());
186 }
187 assert!(out[2].is_some());
188 }
189
190 #[test]
191 fn high_brackets_open_close() {
192 let mut s = SmoothedHeikinAshi::new(3).unwrap();
193 let candles: Vec<Candle> = (0..30)
194 .map(|i| {
195 let b = 100.0 + f64::from(i);
196 c(b, b + 2.0, b - 2.0, b + 0.5)
197 })
198 .collect();
199 for o in s.batch(&candles).into_iter().flatten() {
200 assert!(o.high >= o.open && o.high >= o.close);
201 assert!(o.low <= o.open && o.low <= o.close);
202 }
203 }
204
205 #[test]
206 fn uptrend_close_above_open() {
207 let mut s = SmoothedHeikinAshi::new(3).unwrap();
208 let candles: Vec<Candle> = (0..30)
209 .map(|i| {
210 let b = 100.0 + 2.0 * f64::from(i);
211 c(b, b + 1.0, b - 1.0, b + 0.5)
212 })
213 .collect();
214 let o = s.batch(&candles).into_iter().flatten().last().unwrap();
215 assert!(
216 o.close > o.open,
217 "an uptrend should print a bullish smoothed HA candle"
218 );
219 }
220
221 #[test]
222 fn reset_clears_state() {
223 let mut s = SmoothedHeikinAshi::new(3).unwrap();
224 s.batch(
225 &(0..10)
226 .map(|i| {
227 let b = 100.0 + f64::from(i);
228 c(b, b + 1.0, b - 1.0, b)
229 })
230 .collect::<Vec<_>>(),
231 );
232 assert!(s.is_ready());
233 s.reset();
234 assert!(!s.is_ready());
235 assert_eq!(s.value(), None);
236 assert_eq!(s.update(c(100.0, 101.0, 99.0, 100.0)), None);
237 }
238
239 #[test]
240 fn batch_equals_streaming() {
241 let candles: Vec<Candle> = (0..80)
242 .map(|i| {
243 let b = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
244 c(b, b + 1.0, b - 1.0, b + 0.3)
245 })
246 .collect();
247 let batch = SmoothedHeikinAshi::new(10).unwrap().batch(&candles);
248 let mut b = SmoothedHeikinAshi::new(10).unwrap();
249 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
250 assert_eq!(batch, streamed);
251 }
252}