1use crate::indicators::patterns::candle_settings::*;
6use crate::traits::Next;
7
8#[derive(Debug, Clone)]
12#[allow(non_camel_case_types)]
13pub struct CDLPIERCING {
14 bars_seen: usize,
15 window: CandleWindow,
16 body_long: RollingCandleAvg,
17}
18
19impl CDLPIERCING {
20 pub fn new() -> Self {
21 Self {
22 bars_seen: 0,
23 window: CandleWindow::new(2),
24 body_long: RollingCandleAvg::new(BODY_LONG),
25 }
26 }
27}
28
29impl Default for CDLPIERCING {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl Next<(f64, f64, f64, f64)> for CDLPIERCING {
36 type Output = f64;
37 fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
38 self.bars_seen += 1;
39 self.window.push(open, high, low, close);
40 self.body_long.push(open, high, low, close);
41
42 let lookback = BODY_LONG.avg_period + 1;
43 if self.bars_seen <= lookback {
44 return 0.0;
45 }
46
47 let b1 = self.window.bar(1);
48 let b0 = self.window.bar(0);
49
50 let is_piercing = candle_color(b1.open, b1.close) == -1
51 && real_body(b1.open, b1.close) > self.body_long.val(1)
52 && candle_color(b0.open, b0.close) == 1
53 && real_body(b0.open, b0.close) > self.body_long.val(0)
54 && b0.open < b1.low
55 && b0.close < b1.open
56 && b0.close > b1.close + real_body(b1.open, b1.close) * 0.5;
57
58 if is_piercing { 100.0 } else { 0.0 }
59 }
60}
61
62#[derive(Debug, Clone)]
66#[allow(non_camel_case_types)]
67pub struct CDLSEPARATINGLINES {
68 bars_seen: usize,
69 window: CandleWindow,
70 shadow_vs: RollingCandleAvg,
71 body_long: RollingCandleAvg,
72 equal: RollingCandleAvg,
73}
74
75impl CDLSEPARATINGLINES {
76 pub fn new() -> Self {
77 Self {
78 bars_seen: 0,
79 window: CandleWindow::new(2),
80 shadow_vs: RollingCandleAvg::new(SHADOW_VERY_SHORT),
81 body_long: RollingCandleAvg::new(BODY_LONG),
82 equal: RollingCandleAvg::new(EQUAL),
83 }
84 }
85}
86
87impl Default for CDLSEPARATINGLINES {
88 fn default() -> Self {
89 Self::new()
90 }
91}
92
93impl Next<(f64, f64, f64, f64)> for CDLSEPARATINGLINES {
94 type Output = f64;
95 fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
96 self.bars_seen += 1;
97 self.window.push(open, high, low, close);
98 self.shadow_vs.push(open, high, low, close);
99 self.body_long.push(open, high, low, close);
100 self.equal.push(open, high, low, close);
101
102 let lookback = SHADOW_VERY_SHORT
103 .avg_period
104 .max(BODY_LONG.avg_period)
105 .max(EQUAL.avg_period)
106 + 1;
107 if self.bars_seen <= lookback {
108 return 0.0;
109 }
110
111 let b1 = self.window.bar(1);
112 let b0 = self.window.bar(0);
113
114 let color_prev = candle_color(b1.open, b1.close);
115 let color_curr = candle_color(b0.open, b0.close);
116
117 let base = color_prev != color_curr
118 && (b0.open - b1.open).abs() <= self.equal.val(1)
119 && real_body(b0.open, b0.close) > self.body_long.val(0);
120
121 let bull = base
122 && color_curr == 1
123 && lower_shadow(b0.open, b0.low, b0.close) < self.shadow_vs.val(0);
124 let bear = base
125 && color_curr == -1
126 && upper_shadow(b0.open, b0.high, b0.close) < self.shadow_vs.val(0);
127
128 if bull {
129 100.0
130 } else if bear {
131 -100.0
132 } else {
133 0.0
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
142#[allow(non_camel_case_types)]
143pub struct CDLSHOOTINGSTAR {
144 bars_seen: usize,
145 window: CandleWindow,
146 body_short: RollingCandleAvg,
147 shadow_long: RollingCandleAvg,
148 shadow_vs: RollingCandleAvg,
149}
150
151impl CDLSHOOTINGSTAR {
152 pub fn new() -> Self {
153 Self {
154 bars_seen: 0,
155 window: CandleWindow::new(2),
156 body_short: RollingCandleAvg::new(BODY_SHORT),
157 shadow_long: RollingCandleAvg::new(SHADOW_LONG),
158 shadow_vs: RollingCandleAvg::new(SHADOW_VERY_SHORT),
159 }
160 }
161}
162
163impl Default for CDLSHOOTINGSTAR {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169impl Next<(f64, f64, f64, f64)> for CDLSHOOTINGSTAR {
170 type Output = f64;
171 fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
172 self.bars_seen += 1;
173 self.window.push(open, high, low, close);
174 self.body_short.push(open, high, low, close);
175 self.shadow_long.push(open, high, low, close);
176 self.shadow_vs.push(open, high, low, close);
177
178 let lookback = BODY_SHORT.avg_period.max(SHADOW_VERY_SHORT.avg_period) + 1;
180 if self.bars_seen <= lookback {
181 return 0.0;
182 }
183
184 let b1 = self.window.bar(1);
185 let b0 = self.window.bar(0);
186
187 let gap_up = b0.open.min(b0.close) > b1.open.max(b1.close);
188
189 let is_shooting_star = real_body(b0.open, b0.close) < self.body_short.val(0)
190 && upper_shadow(b0.open, b0.high, b0.close) > self.shadow_long.val(0)
191 && lower_shadow(b0.open, b0.low, b0.close) < self.shadow_vs.val(0)
192 && gap_up;
193
194 if is_shooting_star { -100.0 } else { 0.0 }
195 }
196}
197
198#[derive(Debug, Clone)]
202#[allow(non_camel_case_types)]
203pub struct CDLTHRUSTING {
204 bars_seen: usize,
205 window: CandleWindow,
206 equal: RollingCandleAvg,
207 body_long: RollingCandleAvg,
208}
209
210impl CDLTHRUSTING {
211 pub fn new() -> Self {
212 Self {
213 bars_seen: 0,
214 window: CandleWindow::new(2),
215 equal: RollingCandleAvg::new(EQUAL),
216 body_long: RollingCandleAvg::new(BODY_LONG),
217 }
218 }
219}
220
221impl Default for CDLTHRUSTING {
222 fn default() -> Self {
223 Self::new()
224 }
225}
226
227impl Next<(f64, f64, f64, f64)> for CDLTHRUSTING {
228 type Output = f64;
229 fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
230 self.bars_seen += 1;
231 self.window.push(open, high, low, close);
232 self.equal.push(open, high, low, close);
233 self.body_long.push(open, high, low, close);
234
235 let lookback = EQUAL.avg_period.max(BODY_LONG.avg_period) + 1;
236 if self.bars_seen <= lookback {
237 return 0.0;
238 }
239
240 let b1 = self.window.bar(1);
241 let b0 = self.window.bar(0);
242
243 let is_thrusting = candle_color(b1.open, b1.close) == -1
244 && real_body(b1.open, b1.close) > self.body_long.val(1)
245 && candle_color(b0.open, b0.close) == 1
246 && b0.open < b1.low
247 && b0.close > b1.close + self.equal.val(1)
248 && b0.close <= b1.close + real_body(b1.open, b1.close) * 0.5;
249
250 if is_thrusting { -100.0 } else { 0.0 }
251 }
252}
253
254#[derive(Debug, Clone)]
258#[allow(non_camel_case_types)]
259pub struct CDLKICKING {
260 bars_seen: usize,
261 window: CandleWindow,
262 shadow_vs: RollingCandleAvg,
263 body_long: RollingCandleAvg,
264}
265
266impl CDLKICKING {
267 pub fn new() -> Self {
268 Self {
269 bars_seen: 0,
270 window: CandleWindow::new(2),
271 shadow_vs: RollingCandleAvg::new(SHADOW_VERY_SHORT),
272 body_long: RollingCandleAvg::new(BODY_LONG),
273 }
274 }
275}
276
277impl Default for CDLKICKING {
278 fn default() -> Self {
279 Self::new()
280 }
281}
282
283impl Next<(f64, f64, f64, f64)> for CDLKICKING {
284 type Output = f64;
285 fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
286 self.bars_seen += 1;
287 self.window.push(open, high, low, close);
288 self.shadow_vs.push(open, high, low, close);
289 self.body_long.push(open, high, low, close);
290
291 let lookback = SHADOW_VERY_SHORT.avg_period.max(BODY_LONG.avg_period) + 1;
292 if self.bars_seen <= lookback {
293 return 0.0;
294 }
295
296 let b1 = self.window.bar(1);
297 let b0 = self.window.bar(0);
298
299 let color_prev = candle_color(b1.open, b1.close);
300 let color_curr = candle_color(b0.open, b0.close);
301
302 if color_prev != color_curr
303 && real_body(b1.open, b1.close) > self.body_long.val(1)
304 && upper_shadow(b1.open, b1.high, b1.close) < self.shadow_vs.val(1)
305 && lower_shadow(b1.open, b1.low, b1.close) < self.shadow_vs.val(1)
306 && real_body(b0.open, b0.close) > self.body_long.val(0)
307 && upper_shadow(b0.open, b0.high, b0.close) < self.shadow_vs.val(0)
308 && lower_shadow(b0.open, b0.low, b0.close) < self.shadow_vs.val(0)
309 {
310 let bull = color_prev == -1 && color_curr == 1 && b0.open > b1.open;
311 let bear = color_prev == 1 && color_curr == -1 && b0.open < b1.open;
312 if bull {
313 100.0
314 } else if bear {
315 -100.0
316 } else {
317 0.0
318 }
319 } else {
320 0.0
321 }
322 }
323}
324
325#[derive(Debug, Clone)]
329#[allow(non_camel_case_types)]
330pub struct CDLKICKINGBYLENGTH {
331 bars_seen: usize,
332 window: CandleWindow,
333 shadow_vs: RollingCandleAvg,
334 body_long: RollingCandleAvg,
335}
336
337impl CDLKICKINGBYLENGTH {
338 pub fn new() -> Self {
339 Self {
340 bars_seen: 0,
341 window: CandleWindow::new(2),
342 shadow_vs: RollingCandleAvg::new(SHADOW_VERY_SHORT),
343 body_long: RollingCandleAvg::new(BODY_LONG),
344 }
345 }
346}
347
348impl Default for CDLKICKINGBYLENGTH {
349 fn default() -> Self {
350 Self::new()
351 }
352}
353
354impl Next<(f64, f64, f64, f64)> for CDLKICKINGBYLENGTH {
355 type Output = f64;
356 fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
357 self.bars_seen += 1;
358 self.window.push(open, high, low, close);
359 self.shadow_vs.push(open, high, low, close);
360 self.body_long.push(open, high, low, close);
361
362 let lookback = SHADOW_VERY_SHORT.avg_period.max(BODY_LONG.avg_period) + 1;
363 if self.bars_seen <= lookback {
364 return 0.0;
365 }
366
367 let b1 = self.window.bar(1);
368 let b0 = self.window.bar(0);
369
370 let color_prev = candle_color(b1.open, b1.close);
371 let color_curr = candle_color(b0.open, b0.close);
372
373 if color_prev != color_curr
374 && real_body(b1.open, b1.close) > self.body_long.val(1)
375 && upper_shadow(b1.open, b1.high, b1.close) < self.shadow_vs.val(1)
376 && lower_shadow(b1.open, b1.low, b1.close) < self.shadow_vs.val(1)
377 && real_body(b0.open, b0.close) > self.body_long.val(0)
378 && upper_shadow(b0.open, b0.high, b0.close) < self.shadow_vs.val(0)
379 && lower_shadow(b0.open, b0.low, b0.close) < self.shadow_vs.val(0)
380 {
381 let has_gap = (color_prev == -1 && color_curr == 1 && b0.open > b1.open)
382 || (color_prev == 1 && color_curr == -1 && b0.open < b1.open);
383 let curr_longer = real_body(b0.open, b0.close) >= real_body(b1.open, b1.close);
384 let color = if curr_longer { color_curr } else { color_prev };
385
386 if has_gap { (color as f64) * 100.0 } else { 0.0 }
387 } else {
388 0.0
389 }
390 }
391}
392
393#[derive(Debug, Clone)]
397#[allow(non_camel_case_types)]
398pub struct CDLHIKKAKE {
399 bars_seen: usize,
400 window: CandleWindow,
401 pattern_age: Option<usize>,
403 pattern_result: i32,
404 ref_high: f64,
407 ref_low: f64,
408}
409
410impl CDLHIKKAKE {
411 pub fn new() -> Self {
412 Self {
413 bars_seen: 0,
414 window: CandleWindow::new(3),
415 pattern_age: None,
416 pattern_result: 0,
417 ref_high: 0.0,
418 ref_low: 0.0,
419 }
420 }
421
422 fn confirm(&mut self, close: f64) -> f64 {
424 if let Some(age) = self.pattern_age
425 && (1..=3).contains(&age)
426 {
427 {
428 if self.pattern_result > 0 && close > self.ref_high {
429 self.pattern_age = None;
430 return (self.pattern_result + 100) as f64;
431 } else if self.pattern_result < 0 && close < self.ref_low {
432 self.pattern_age = None;
433 return (self.pattern_result - 100) as f64;
434 }
435 }
436 }
437 0.0
438 }
439}
440
441impl Default for CDLHIKKAKE {
442 fn default() -> Self {
443 Self::new()
444 }
445}
446
447impl Next<(f64, f64, f64, f64)> for CDLHIKKAKE {
448 type Output = f64;
449
450 fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
451 const LOOKBACK: usize = 5;
452 self.bars_seen += 1;
453 self.window.push(open, high, low, close);
454
455 if let Some(age) = self.pattern_age {
456 self.pattern_age = Some(age + 1);
457 }
458
459 if self.window.len() < 3 {
460 return 0.0;
461 }
462
463 let b0 = self.window.bar(0);
464 let b1 = self.window.bar(1);
465 let b2 = self.window.bar(2);
466
467 let emitting = self.bars_seen > LOOKBACK;
470
471 let inside = b1.high < b2.high && b1.low > b2.low;
472 if inside && b0.high < b1.high && b0.low < b1.low {
473 self.pattern_result = 100;
474 self.pattern_age = Some(0);
475 self.ref_high = b1.high;
476 self.ref_low = b1.low;
477 return if emitting { 100.0 } else { 0.0 };
478 }
479 if inside && b0.high > b1.high && b0.low > b1.low {
480 self.pattern_result = -100;
481 self.pattern_age = Some(0);
482 self.ref_high = b1.high;
483 self.ref_low = b1.low;
484 return if emitting { -100.0 } else { 0.0 };
485 }
486
487 if !emitting {
488 return 0.0;
489 }
490 self.confirm(b0.close)
491 }
492}
493
494#[derive(Debug, Clone)]
498#[allow(non_camel_case_types)]
499pub struct CDLHIKKAKEMOD {
500 bars_seen: usize,
501 window: CandleWindow,
502 near_avg: RollingCandleAvg,
503 pattern_age: Option<usize>,
505 pattern_result: i32,
506 ref_high: f64,
507 ref_low: f64,
508}
509
510impl CDLHIKKAKEMOD {
511 pub fn new() -> Self {
512 Self {
513 bars_seen: 0,
514 window: CandleWindow::new(4),
515 near_avg: RollingCandleAvg::new(NEAR),
516 pattern_age: None,
517 pattern_result: 0,
518 ref_high: 0.0,
519 ref_low: 0.0,
520 }
521 }
522}
523
524impl Default for CDLHIKKAKEMOD {
525 fn default() -> Self {
526 Self::new()
527 }
528}
529
530impl Next<(f64, f64, f64, f64)> for CDLHIKKAKEMOD {
531 type Output = f64;
532
533 fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
534 self.bars_seen += 1;
535 self.window.push(open, high, low, close);
536 self.near_avg.push(open, high, low, close);
537
538 if let Some(age) = self.pattern_age {
539 self.pattern_age = Some(age + 1);
540 }
541
542 let lookback = 1usize.max(NEAR.avg_period) + 5;
544 if self.bars_seen <= lookback {
545 return 0.0;
546 }
547
548 let b0 = self.window.bar(0);
549 let b1 = self.window.bar(1);
550 let b2 = self.window.bar(2);
551 let b3 = self.window.bar(3);
552
553 if b1.high < b2.high && b1.low > b2.low && b2.high < b3.high && b2.low > b3.low {
555 let near_avg = self.near_avg.val(2);
556 if b0.high < b1.high && b0.low < b1.low && b2.close <= b2.low + near_avg {
557 self.pattern_result = 100;
558 self.pattern_age = Some(0);
559 self.ref_high = b1.high;
560 self.ref_low = b1.low;
561 } else if b0.high > b1.high && b0.low > b1.low && b2.close >= b2.high - near_avg {
562 self.pattern_result = -100;
563 self.pattern_age = Some(0);
564 self.ref_high = b1.high;
565 self.ref_low = b1.low;
566 }
567 }
568
569 if let Some(age) = self.pattern_age
572 && age <= 3
573 {
574 {
575 if self.pattern_result > 0 && b0.close > self.ref_high {
576 self.pattern_age = None;
577 return (self.pattern_result + 100) as f64;
578 } else if self.pattern_result < 0 && b0.close < self.ref_low {
579 self.pattern_age = None;
580 return (self.pattern_result - 100) as f64;
581 }
582 }
583 }
584 0.0
585 }
586}
587
588#[derive(Debug, Clone)]
592#[allow(non_camel_case_types)]
593pub struct CDLSTICKSANDWICH {
594 bars_seen: usize,
595 window: CandleWindow,
596 equal: RollingCandleAvg,
597}
598
599impl CDLSTICKSANDWICH {
600 pub fn new() -> Self {
601 Self {
602 bars_seen: 0,
603 window: CandleWindow::new(3),
604 equal: RollingCandleAvg::new(EQUAL),
605 }
606 }
607}
608
609impl Default for CDLSTICKSANDWICH {
610 fn default() -> Self {
611 Self::new()
612 }
613}
614
615impl Next<(f64, f64, f64, f64)> for CDLSTICKSANDWICH {
616 type Output = f64;
617 fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
618 self.bars_seen += 1;
619 self.window.push(open, high, low, close);
620 self.equal.push(open, high, low, close);
621
622 let lookback = EQUAL.avg_period + 2;
623 if self.bars_seen <= lookback {
624 return 0.0;
625 }
626
627 let b2 = self.window.bar(2);
628 let b1 = self.window.bar(1);
629 let b0 = self.window.bar(0);
630
631 let is_sandwich = candle_color(b2.open, b2.close) == -1
632 && candle_color(b1.open, b1.close) == 1
633 && candle_color(b0.open, b0.close) == -1
634 && b1.low > b2.close
635 && (b0.close - b2.close).abs() <= self.equal.val(2);
636
637 if is_sandwich { 100.0 } else { 0.0 }
638 }
639}
640
641#[derive(Debug, Clone)]
645#[allow(non_camel_case_types)]
646pub struct CDL2CROWS {
647 bars_seen: usize,
648 window: CandleWindow,
649 body_long: RollingCandleAvg,
650}
651
652impl CDL2CROWS {
653 pub fn new() -> Self {
654 Self {
655 bars_seen: 0,
656 window: CandleWindow::new(3),
657 body_long: RollingCandleAvg::new(BODY_LONG),
658 }
659 }
660}
661
662impl Default for CDL2CROWS {
663 fn default() -> Self {
664 Self::new()
665 }
666}
667
668impl Next<(f64, f64, f64, f64)> for CDL2CROWS {
669 type Output = f64;
670 fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
671 self.bars_seen += 1;
672 self.window.push(open, high, low, close);
673 self.body_long.push(open, high, low, close);
674
675 let lookback = BODY_LONG.avg_period + 2;
676 if self.bars_seen <= lookback {
677 return 0.0;
678 }
679
680 let b2 = self.window.bar(2);
681 let b1 = self.window.bar(1);
682 let b0 = self.window.bar(0);
683
684 let is_2crows = candle_color(b2.open, b2.close) == 1
685 && real_body(b2.open, b2.close) > self.body_long.val(2)
686 && candle_color(b1.open, b1.close) == -1
687 && b1.open.min(b1.close) > b2.open.max(b2.close) && candle_color(b0.open, b0.close) == -1
689 && b0.open < b1.open && b0.open > b1.close
690 && b0.close > b2.open && b0.close < b2.close;
691
692 if is_2crows { -100.0 } else { 0.0 }
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699 use crate::indicators::patterns::fixtures as fx;
700 use crate::test_pattern_parity;
701
702 test_pattern_parity!(
703 test_cdlpiercing,
704 CDLPIERCING,
705 talib_rs::pattern::cdl_piercing,
706 fx::piercing
707 );
708 test_pattern_parity!(
709 test_cdlseparatinglines,
710 CDLSEPARATINGLINES,
711 talib_rs::pattern::cdl_separatinglines
712 );
713 test_pattern_parity!(
714 test_cdlshootingstar,
715 CDLSHOOTINGSTAR,
716 talib_rs::pattern::cdl_shootingstar,
717 fx::shootingstar
718 );
719 test_pattern_parity!(
720 test_cdlthrusting,
721 CDLTHRUSTING,
722 talib_rs::pattern::cdl_thrusting,
723 fx::thrusting
724 );
725 test_pattern_parity!(
726 test_cdlkicking,
727 CDLKICKING,
728 talib_rs::pattern::cdl_kicking,
729 fx::kicking
730 );
731 test_pattern_parity!(
732 test_cdlkickingbylength,
733 CDLKICKINGBYLENGTH,
734 talib_rs::pattern::cdl_kickingbylength,
735 fx::kickingbylength
736 );
737 test_pattern_parity!(test_cdlhikkake, CDLHIKKAKE, talib_rs::pattern::cdl_hikkake);
738 test_pattern_parity!(
768 test_cdlhikkakemod,
769 CDLHIKKAKEMOD,
770 talib_rs::pattern::cdl_hikkakemod,
771 fx::hikkakemod,
772 oracle_exempt = "talib-rs's cdl_hikkakemod NEAR rolling sum is non-contiguous: it \
773 seeds the window one bar early and then updates it with a rule that \
774 does not match the seed, leaving 6 terms divided by 5 with one \
775 ancient bar pinned in the sum forever. An independent model of C \
776 TA-Lib semantics matches the native implementation exactly (0/200,000) \
777 and the oracle at only 4 bars.",
778 max_mismatches = 8,
779 reference_mismatches = 4
780 );
781 test_pattern_parity!(
782 test_cdlsticksandwich,
783 CDLSTICKSANDWICH,
784 talib_rs::pattern::cdl_sticksandwich,
785 fx::sticksandwich
786 );
787 test_pattern_parity!(
788 test_cdl2crows,
789 CDL2CROWS,
790 talib_rs::pattern::cdl_2crows,
791 fx::two_crows
792 );
793}