1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6const NEAR: f64 = 0.2;
8
9#[derive(Debug, Clone, Default)]
50pub struct HikkakeModified {
51 c1: Option<Candle>,
52 c2: Option<Candle>,
53 c3: Option<Candle>,
54 has_emitted: bool,
55}
56
57impl HikkakeModified {
58 pub const fn new() -> Self {
60 Self {
61 c1: None,
62 c2: None,
63 c3: None,
64 has_emitted: false,
65 }
66 }
67}
68
69impl Indicator for HikkakeModified {
70 type Input = Candle;
71 type Output = f64;
72
73 #[inline]
74 fn update(&mut self, candle: Candle) -> Option<f64> {
75 let (bar1, bar2, bar3) = (self.c1, self.c2, self.c3);
76 self.c1 = self.c2;
77 self.c2 = self.c3;
78 self.c3 = Some(candle);
79 let (Some(bar1), Some(bar2), Some(bar3)) = (bar1, bar2, bar3) else {
80 return None;
81 };
82 self.has_emitted = true;
83 if !(bar2.high < bar1.high
85 && bar2.low > bar1.low
86 && bar3.high < bar2.high
87 && bar3.low > bar2.low)
88 {
89 return Some(0.0);
90 }
91 let near = NEAR * (bar2.high - bar2.low);
92 if candle.high < bar3.high && candle.low < bar3.low && bar2.close <= bar2.low + near {
94 return Some(1.0);
95 }
96 if candle.high > bar3.high && candle.low > bar3.low && bar2.close >= bar2.high - near {
98 return Some(-1.0);
99 }
100 Some(0.0)
101 }
102
103 fn reset(&mut self) {
104 self.c1 = None;
105 self.c2 = None;
106 self.c3 = None;
107 self.has_emitted = false;
108 }
109
110 #[inline]
111 fn warmup_period(&self) -> usize {
112 4
113 }
114
115 #[inline]
116 fn is_ready(&self) -> bool {
117 self.has_emitted
118 }
119
120 #[inline]
121 fn name(&self) -> &'static str {
122 "HikkakeModified"
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::traits::BatchExt;
130
131 fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
132 Candle::new(open, high, low, close, 1.0, ts).unwrap()
133 }
134
135 #[test]
136 fn accessors_and_metadata() {
137 let t = HikkakeModified::new();
138 assert_eq!(t.name(), "HikkakeModified");
139 assert_eq!(t.warmup_period(), 4);
140 assert!(!t.is_ready());
141 }
142
143 #[test]
144 fn bullish_modified_hikkake_is_plus_one() {
145 let mut t = HikkakeModified::new();
146 assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), None);
147 assert_eq!(t.update(c(11.0, 13.0, 7.0, 7.5, 1)), None);
148 assert_eq!(t.update(c(9.0, 12.0, 8.0, 10.0, 2)), None);
149 assert_eq!(t.update(c(9.0, 11.0, 6.0, 9.0, 3)), Some(1.0));
150 }
151
152 #[test]
153 fn bearish_modified_hikkake_is_minus_one() {
154 let mut t = HikkakeModified::new();
155 assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), None);
156 assert_eq!(t.update(c(11.0, 13.0, 7.0, 12.5, 1)), None);
157 assert_eq!(t.update(c(9.0, 12.0, 8.0, 10.0, 2)), None);
158 assert_eq!(t.update(c(11.0, 14.0, 9.0, 13.0, 3)), Some(-1.0));
159 }
160
161 #[test]
162 fn second_bar_close_not_near_extreme_yields_zero() {
163 let mut t = HikkakeModified::new();
164 t.update(c(10.0, 15.0, 5.0, 12.0, 0));
165 t.update(c(11.0, 13.0, 7.0, 10.0, 1));
167 t.update(c(9.0, 12.0, 8.0, 10.0, 2));
168 assert_eq!(t.update(c(9.0, 11.0, 6.0, 9.0, 3)), Some(0.0));
169 }
170
171 #[test]
172 fn not_double_inside_bar_yields_zero() {
173 let mut t = HikkakeModified::new();
174 t.update(c(10.0, 15.0, 5.0, 12.0, 0));
175 t.update(c(11.0, 13.0, 7.0, 7.5, 1));
176 t.update(c(9.0, 14.0, 8.0, 10.0, 2));
178 assert_eq!(t.update(c(9.0, 11.0, 6.0, 9.0, 3)), Some(0.0));
179 }
180
181 #[test]
182 fn first_three_bars_return_none() {
183 let mut t = HikkakeModified::new();
184 assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), None);
185 assert_eq!(t.update(c(11.0, 13.0, 7.0, 7.5, 1)), None);
186 assert_eq!(t.update(c(9.0, 12.0, 8.0, 10.0, 2)), None);
187 }
188
189 #[test]
190 fn batch_equals_streaming() {
191 let candles: Vec<Candle> = (0..40)
192 .map(|i| {
193 let base = 100.0 + i as f64;
194 match i % 4 {
195 0 => c(base, base + 6.0, base - 6.0, base, i),
196 1 => c(base, base + 4.0, base - 4.0, base - 3.5, i),
197 2 => c(base, base + 2.0, base - 2.0, base, i),
198 _ => c(base, base + 1.0, base - 5.0, base, i),
199 }
200 })
201 .collect();
202 let mut a = HikkakeModified::new();
203 let mut b = HikkakeModified::new();
204 assert_eq!(
205 a.batch(&candles),
206 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
207 );
208 }
209
210 #[test]
211 fn reset_clears_state() {
212 let mut t = HikkakeModified::new();
213 t.update(c(10.0, 15.0, 5.0, 12.0, 0));
214 t.update(c(11.0, 13.0, 7.0, 7.5, 1));
215 t.update(c(9.0, 12.0, 8.0, 10.0, 2));
216 t.update(c(9.0, 11.0, 6.0, 9.0, 3));
217 assert!(t.is_ready());
218 t.reset();
219 assert!(!t.is_ready());
220 assert_eq!(t.update(c(10.0, 15.0, 5.0, 12.0, 0)), None);
221 }
222
223 fn run(bars: [Candle; 4]) -> Option<f64> {
225 let mut t = HikkakeModified::new();
226 bars.iter().map(|b| t.update(*b)).last().unwrap()
227 }
228
229 const BAR1: (f64, f64, f64, f64) = (10.0, 15.0, 5.0, 12.0);
231 const BAR3: (f64, f64, f64, f64) = (10.0, 12.0, 9.0, 10.0);
232
233 fn bar(t: (f64, f64, f64, f64), ts: i64) -> Candle {
234 c(t.0, t.1, t.2, t.3, ts)
235 }
236
237 #[test]
238 fn hand_computed_near_boundary_bullish() {
239 let bar4 = c(9.0, 11.0, 7.0, 9.0, 3);
242 assert_eq!(
243 run([bar(BAR1, 0), c(11.0, 13.0, 8.0, 9.0, 1), bar(BAR3, 2), bar4]),
244 Some(1.0)
245 );
246 assert_eq!(
247 run([bar(BAR1, 0), c(11.0, 13.0, 8.0, 8.5, 1), bar(BAR3, 2), bar4]),
248 Some(1.0)
249 );
250 assert_eq!(
252 run([
253 bar(BAR1, 0),
254 c(11.0, 13.0, 8.0, 9.01, 1),
255 bar(BAR3, 2),
256 bar4
257 ]),
258 Some(0.0)
259 );
260 }
261
262 #[test]
263 fn hand_computed_near_boundary_bearish() {
264 let bar4 = c(11.0, 14.0, 10.0, 13.0, 3);
266 assert_eq!(
267 run([
268 bar(BAR1, 0),
269 c(11.0, 13.0, 8.0, 12.0, 1),
270 bar(BAR3, 2),
271 bar4
272 ]),
273 Some(-1.0)
274 );
275 assert_eq!(
277 run([
278 bar(BAR1, 0),
279 c(11.0, 13.0, 8.0, 11.99, 1),
280 bar(BAR3, 2),
281 bar4
282 ]),
283 Some(0.0)
284 );
285 }
286
287 #[test]
288 fn bullish_break_fails_each_condition() {
289 let bar2 = c(11.0, 13.0, 8.0, 9.0, 1);
290 assert_eq!(
292 run([bar(BAR1, 0), bar2, bar(BAR3, 2), c(9.0, 12.0, 7.0, 9.0, 3)]),
293 Some(0.0)
294 );
295 assert_eq!(
297 run([
298 bar(BAR1, 0),
299 bar2,
300 bar(BAR3, 2),
301 c(10.0, 11.0, 9.0, 10.0, 3)
302 ]),
303 Some(0.0)
304 );
305 assert_eq!(
307 run([
308 bar(BAR1, 0),
309 bar2,
310 bar(BAR3, 2),
311 c(10.0, 13.0, 7.0, 10.0, 3)
312 ]),
313 Some(0.0)
314 );
315 assert_eq!(
317 run([
318 bar(BAR1, 0),
319 bar2,
320 bar(BAR3, 2),
321 c(10.0, 11.0, 9.5, 10.0, 3)
322 ]),
323 Some(0.0)
324 );
325 }
326
327 #[test]
328 fn bearish_break_fails_each_condition() {
329 let bar2 = c(11.0, 13.0, 8.0, 12.0, 1);
330 assert_eq!(
332 run([
333 bar(BAR1, 0),
334 bar2,
335 bar(BAR3, 2),
336 c(11.0, 12.0, 10.0, 11.0, 3)
337 ]),
338 Some(0.0)
339 );
340 assert_eq!(
342 run([
343 bar(BAR1, 0),
344 bar2,
345 bar(BAR3, 2),
346 c(11.0, 14.0, 9.0, 13.0, 3)
347 ]),
348 Some(0.0)
349 );
350 assert_eq!(
352 run([bar(BAR1, 0), bar2, bar(BAR3, 2), c(9.0, 11.0, 7.0, 9.0, 3)]),
353 Some(0.0)
354 );
355 }
356
357 #[test]
358 fn each_inside_bar_condition_is_required() {
359 let bar4 = c(9.0, 11.0, 7.0, 9.0, 3);
360 assert_eq!(
362 run([bar(BAR1, 0), c(11.0, 15.0, 8.0, 9.0, 1), bar(BAR3, 2), bar4]),
363 Some(0.0)
364 );
365 assert_eq!(
367 run([bar(BAR1, 0), c(11.0, 13.0, 5.0, 6.0, 1), bar(BAR3, 2), bar4]),
368 Some(0.0)
369 );
370 let bar2 = c(11.0, 13.0, 8.0, 9.0, 1);
372 assert_eq!(
373 run([bar(BAR1, 0), bar2, c(10.0, 13.0, 9.0, 10.0, 2), bar4]),
374 Some(0.0)
375 );
376 assert_eq!(
378 run([bar(BAR1, 0), bar2, c(10.0, 12.0, 8.0, 10.0, 2), bar4]),
379 Some(0.0)
380 );
381 }
382
383 #[test]
384 fn first_value_lands_exactly_at_warmup_index() {
385 let bars = [
386 bar(BAR1, 0),
387 c(11.0, 13.0, 8.0, 9.0, 1),
388 bar(BAR3, 2),
389 c(9.0, 11.0, 7.0, 9.0, 3),
390 c(9.0, 11.0, 7.0, 9.0, 4),
391 ];
392 let mut t = HikkakeModified::new();
393 let out = t.batch(&bars);
394 let warm = t.warmup_period();
395 assert!(out[..warm - 1].iter().all(Option::is_none));
396 assert_eq!(out[warm - 1], Some(1.0));
397 assert_eq!(out[warm], Some(0.0));
398 }
399
400 fn mixed_series() -> Vec<Candle> {
401 (0..40)
402 .map(|i| {
403 let base = 100.0 + f64::from(i % 7);
404 match i % 4 {
405 0 => c(base, base + 6.0, base - 6.0, base, i64::from(i)),
406 1 => c(base, base + 4.0, base - 4.0, base - 3.5, i64::from(i)),
407 2 => c(base, base + 2.0, base - 2.0, base, i64::from(i)),
408 _ => c(base, base + 1.0, base - 5.0, base, i64::from(i)),
409 }
410 })
411 .collect()
412 }
413
414 #[test]
415 fn reset_replays_identically() {
416 let candles = mixed_series();
417 let fresh = HikkakeModified::new().batch(&candles);
418 let mut t = HikkakeModified::new();
419 let _ = t.batch(&candles);
420 t.reset();
421 assert_eq!(t.batch(&candles), fresh);
422 }
423
424 #[test]
425 fn batch_nan_into_matches_streaming_bits() {
426 let candles = mixed_series();
427 let mut t = HikkakeModified::new();
428 let streamed: Vec<f64> = candles
429 .iter()
430 .map(|x| t.update(*x).unwrap_or(f64::NAN))
431 .collect();
432 let mut out = vec![0.0; candles.len()];
433 HikkakeModified::new().batch_nan_into(&candles, &mut out);
434 assert!(streamed
435 .iter()
436 .zip(&out)
437 .all(|(a, b)| a.to_bits() == b.to_bits()));
438 }
439}