Skip to main content

ta_indicators/
candles.rs

1//! Candlestick pattern recognition — warmup-exact ports of the TA-Lib `CDL*`
2//! family.
3//!
4//! TA-Lib drives every pattern off a small set of global "candle settings",
5//! each describing how to size a body or shadow relative to a trailing average
6//! of a chosen range type. We replicate the C library's default settings and
7//! its averaging semantics exactly:
8//!
9//! * `CANDLERANGE(setting, i)` is the raw range (real body, high-low, or the
10//!   sum of both shadows) at bar `i`.
11//! * `CANDLEAVERAGE(setting, i)` is
12//!   `factor * (avg_period != 0 ? mean(range over [i-avg_period, i-1]) : range(i)) / shadow_div`
13//!   where `shadow_div` is `2.0` for the `Shadows` range type, else `1.0`.
14//!
15//! Output matches TA-Lib: an `i32` per bar of `-100`, `0`, or `+100`, with the
16//! warmup region (`< lookback`) zero-filled. The parity harness compares every
17//! bar, so both the lookback and the values must be exact.
18
19#[derive(Clone, Copy)]
20enum RangeType {
21    RealBody,
22    HighLow,
23    Shadows,
24}
25
26#[derive(Clone, Copy)]
27struct CandleSetting {
28    range: RangeType,
29    avg_period: usize,
30    factor: f64,
31}
32
33// TA-Lib `TA_CandleDefaultSettings` (ta_global.c).
34const BODY_LONG: CandleSetting = CandleSetting {
35    range: RangeType::RealBody,
36    avg_period: 10,
37    factor: 1.0,
38};
39const BODY_SHORT: CandleSetting = CandleSetting {
40    range: RangeType::RealBody,
41    avg_period: 10,
42    factor: 1.0,
43};
44const BODY_DOJI: CandleSetting = CandleSetting {
45    range: RangeType::HighLow,
46    avg_period: 10,
47    factor: 0.1,
48};
49const SHADOW_LONG: CandleSetting = CandleSetting {
50    range: RangeType::RealBody,
51    avg_period: 0,
52    factor: 1.0,
53};
54const SHADOW_VERY_LONG: CandleSetting = CandleSetting {
55    range: RangeType::RealBody,
56    avg_period: 0,
57    factor: 2.0,
58};
59const SHADOW_SHORT: CandleSetting = CandleSetting {
60    range: RangeType::Shadows,
61    avg_period: 10,
62    factor: 1.0,
63};
64const SHADOW_VERY_SHORT: CandleSetting = CandleSetting {
65    range: RangeType::HighLow,
66    avg_period: 10,
67    factor: 0.1,
68};
69const NEAR: CandleSetting = CandleSetting {
70    range: RangeType::HighLow,
71    avg_period: 5,
72    factor: 0.2,
73};
74const FAR: CandleSetting = CandleSetting {
75    range: RangeType::HighLow,
76    avg_period: 5,
77    factor: 0.6,
78};
79const EQUAL: CandleSetting = CandleSetting {
80    range: RangeType::HighLow,
81    avg_period: 5,
82    factor: 0.05,
83};
84
85/// Bundled OHLC views plus the candle primitives TA-Lib builds patterns from.
86struct Candles<'a> {
87    open: &'a [f64],
88    high: &'a [f64],
89    low: &'a [f64],
90    close: &'a [f64],
91    len: usize,
92}
93
94impl<'a> Candles<'a> {
95    fn new(open: &'a [f64], high: &'a [f64], low: &'a [f64], close: &'a [f64]) -> Self {
96        let len = open.len().min(high.len()).min(low.len()).min(close.len());
97        Self {
98            open,
99            high,
100            low,
101            close,
102            len,
103        }
104    }
105
106    #[inline]
107    fn real_body(&self, i: usize) -> f64 {
108        (self.close[i] - self.open[i]).abs()
109    }
110
111    #[inline]
112    fn upper_shadow(&self, i: usize) -> f64 {
113        self.high[i] - self.open[i].max(self.close[i])
114    }
115
116    #[inline]
117    fn lower_shadow(&self, i: usize) -> f64 {
118        self.open[i].min(self.close[i]) - self.low[i]
119    }
120
121    #[inline]
122    fn high_low(&self, i: usize) -> f64 {
123        self.high[i] - self.low[i]
124    }
125
126    /// +1 white (close >= open), -1 black.
127    #[inline]
128    fn color(&self, i: usize) -> i32 {
129        if self.close[i] >= self.open[i] { 1 } else { -1 }
130    }
131
132    /// Top of the real body (`max(open, close)`).
133    #[inline]
134    fn body_top(&self, i: usize) -> f64 {
135        self.open[i].max(self.close[i])
136    }
137
138    /// Bottom of the real body (`min(open, close)`).
139    #[inline]
140    fn body_bottom(&self, i: usize) -> f64 {
141        self.open[i].min(self.close[i])
142    }
143
144    /// `TA_REALBODYGAPUP`: candle `i2`'s real body opens entirely above `i1`'s.
145    #[inline]
146    fn real_body_gap_up(&self, i2: usize, i1: usize) -> bool {
147        self.body_bottom(i2) > self.body_top(i1)
148    }
149
150    /// `TA_REALBODYGAPDOWN`: candle `i2`'s real body sits entirely below `i1`'s.
151    #[inline]
152    fn real_body_gap_down(&self, i2: usize, i1: usize) -> bool {
153        self.body_top(i2) < self.body_bottom(i1)
154    }
155
156    /// `TA_CANDLEGAPUP`: candle `i2`'s low is above `i1`'s high.
157    #[inline]
158    fn candle_gap_up(&self, i2: usize, i1: usize) -> bool {
159        self.low[i2] > self.high[i1]
160    }
161
162    /// `TA_CANDLEGAPDOWN`: candle `i2`'s high is below `i1`'s low.
163    #[inline]
164    fn candle_gap_down(&self, i2: usize, i1: usize) -> bool {
165        self.high[i2] < self.low[i1]
166    }
167
168    fn range(&self, setting: &CandleSetting, i: usize) -> f64 {
169        match setting.range {
170            RangeType::RealBody => self.real_body(i),
171            RangeType::HighLow => self.high_low(i),
172            RangeType::Shadows => self.upper_shadow(i) + self.lower_shadow(i),
173        }
174    }
175
176    /// `CANDLEAVERAGE` evaluated for the setting applied to candle `i`. The
177    /// trailing window is `[i - avg_period, i - 1]` (exclusive of `i`), matching
178    /// TA-Lib's primed running totals.
179    fn average(&self, setting: &CandleSetting, i: usize) -> f64 {
180        let base = if setting.avg_period == 0 {
181            self.range(setting, i)
182        } else {
183            let mut sum = 0.0;
184            for k in (i - setting.avg_period)..i {
185                sum += self.range(setting, k);
186            }
187            sum / setting.avg_period as f64
188        };
189        let divisor = if matches!(setting.range, RangeType::Shadows) {
190            2.0
191        } else {
192            1.0
193        };
194        setting.factor * base / divisor
195    }
196}
197
198/// Run a per-bar pattern test from `lookback` onward, zero-filling the warmup
199/// region to mirror TA-Lib's Python output.
200fn scan(len: usize, lookback: usize, mut test: impl FnMut(usize) -> i32) -> Vec<i32> {
201    let mut out = vec![0i32; len];
202    if lookback < len {
203        for (i, slot) in out.iter_mut().enumerate().skip(lookback) {
204            *slot = test(i);
205        }
206    }
207    out
208}
209
210/// Doji: real body no larger than the doji-body average.
211pub fn cdl_doji(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
212    let k = Candles::new(open, high, low, close);
213    scan(k.len, BODY_DOJI.avg_period, |i| {
214        if k.real_body(i) <= k.average(&BODY_DOJI, i) {
215            100
216        } else {
217            0
218        }
219    })
220}
221
222/// Dragonfly doji: doji body with a negligible upper shadow and a long lower shadow.
223pub fn cdl_dragonfly_doji(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
224    let k = Candles::new(open, high, low, close);
225    scan(
226        k.len,
227        BODY_DOJI.avg_period.max(SHADOW_VERY_SHORT.avg_period),
228        |i| {
229            let svs = k.average(&SHADOW_VERY_SHORT, i);
230            if k.real_body(i) <= k.average(&BODY_DOJI, i)
231                && k.upper_shadow(i) < svs
232                && k.lower_shadow(i) > svs
233            {
234                100
235            } else {
236                0
237            }
238        },
239    )
240}
241
242/// Gravestone doji: doji body with a negligible lower shadow and a long upper shadow.
243pub fn cdl_gravestone_doji(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
244    let k = Candles::new(open, high, low, close);
245    scan(
246        k.len,
247        BODY_DOJI.avg_period.max(SHADOW_VERY_SHORT.avg_period),
248        |i| {
249            let svs = k.average(&SHADOW_VERY_SHORT, i);
250            if k.real_body(i) <= k.average(&BODY_DOJI, i)
251                && k.lower_shadow(i) < svs
252                && k.upper_shadow(i) > svs
253            {
254                100
255            } else {
256                0
257            }
258        },
259    )
260}
261
262/// Long-legged doji: doji body with at least one long shadow.
263pub fn cdl_long_legged_doji(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
264    let k = Candles::new(open, high, low, close);
265    scan(
266        k.len,
267        BODY_DOJI.avg_period.max(SHADOW_LONG.avg_period),
268        |i| {
269            let shadow_long = k.average(&SHADOW_LONG, i);
270            if k.real_body(i) <= k.average(&BODY_DOJI, i)
271                && (k.upper_shadow(i) > shadow_long || k.lower_shadow(i) > shadow_long)
272            {
273                100
274            } else {
275                0
276            }
277        },
278    )
279}
280
281/// Marubozu: long body with negligible shadows on both ends.
282pub fn cdl_marubozu(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
283    let k = Candles::new(open, high, low, close);
284    scan(
285        k.len,
286        BODY_LONG.avg_period.max(SHADOW_VERY_SHORT.avg_period),
287        |i| {
288            let svs = k.average(&SHADOW_VERY_SHORT, i);
289            if k.real_body(i) > k.average(&BODY_LONG, i)
290                && k.upper_shadow(i) < svs
291                && k.lower_shadow(i) < svs
292            {
293                100 * k.color(i)
294            } else {
295                0
296            }
297        },
298    )
299}
300
301/// Closing marubozu: long body with no shadow on the closing end.
302pub fn cdl_closing_marubozu(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
303    let k = Candles::new(open, high, low, close);
304    scan(
305        k.len,
306        BODY_LONG.avg_period.max(SHADOW_VERY_SHORT.avg_period),
307        |i| {
308            let svs = k.average(&SHADOW_VERY_SHORT, i);
309            if k.real_body(i) > k.average(&BODY_LONG, i)
310                && ((k.color(i) == 1 && k.upper_shadow(i) < svs)
311                    || (k.color(i) == -1 && k.lower_shadow(i) < svs))
312            {
313                100 * k.color(i)
314            } else {
315                0
316            }
317        },
318    )
319}
320
321/// Long white/black line: long body with short shadows.
322pub fn cdl_long_line(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
323    let k = Candles::new(open, high, low, close);
324    scan(
325        k.len,
326        BODY_LONG.avg_period.max(SHADOW_SHORT.avg_period),
327        |i| {
328            let short = k.average(&SHADOW_SHORT, i);
329            if k.real_body(i) > k.average(&BODY_LONG, i)
330                && k.upper_shadow(i) < short
331                && k.lower_shadow(i) < short
332            {
333                100 * k.color(i)
334            } else {
335                0
336            }
337        },
338    )
339}
340
341/// Short line: short body with short shadows.
342pub fn cdl_short_line(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
343    let k = Candles::new(open, high, low, close);
344    scan(
345        k.len,
346        BODY_SHORT.avg_period.max(SHADOW_SHORT.avg_period),
347        |i| {
348            let short = k.average(&SHADOW_SHORT, i);
349            if k.real_body(i) < k.average(&BODY_SHORT, i)
350                && k.upper_shadow(i) < short
351                && k.lower_shadow(i) < short
352            {
353                100 * k.color(i)
354            } else {
355                0
356            }
357        },
358    )
359}
360
361/// Spinning top: small body with both shadows longer than the body.
362pub fn cdl_spinning_top(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
363    let k = Candles::new(open, high, low, close);
364    scan(k.len, BODY_SHORT.avg_period, |i| {
365        let body = k.real_body(i);
366        if body < k.average(&BODY_SHORT, i) && k.upper_shadow(i) > body && k.lower_shadow(i) > body
367        {
368            100 * k.color(i)
369        } else {
370            0
371        }
372    })
373}
374
375/// High-wave: small body with very long shadows on both sides.
376pub fn cdl_high_wave(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
377    let k = Candles::new(open, high, low, close);
378    scan(
379        k.len,
380        BODY_SHORT.avg_period.max(SHADOW_VERY_LONG.avg_period),
381        |i| {
382            let very_long = k.average(&SHADOW_VERY_LONG, i);
383            if k.real_body(i) < k.average(&BODY_SHORT, i)
384                && k.upper_shadow(i) > very_long
385                && k.lower_shadow(i) > very_long
386            {
387                100 * k.color(i)
388            } else {
389                0
390            }
391        },
392    )
393}
394
395// ---------------------------------------------------------------------------
396// Multi-candle patterns. Penetration parameters use TA-Lib's documented
397// defaults: 0.3 for the abandoned-baby / morning- / evening-star family and
398// 0.5 for dark cloud cover and mat hold.
399// ---------------------------------------------------------------------------
400
401/// Two crows: long white, then a black gapping up, then a black opening within
402/// the second body and closing within the first body. Always bearish.
403pub fn cdl_2crows(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
404    let k = Candles::new(open, high, low, close);
405    scan(k.len, BODY_LONG.avg_period + 2, |i| {
406        if k.color(i - 2) == 1
407            && k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
408            && k.color(i - 1) == -1
409            && k.real_body_gap_up(i - 1, i - 2)
410            && k.color(i) == -1
411            && k.open[i] < k.open[i - 1]
412            && k.open[i] > k.close[i - 1]
413            && k.close[i] > k.open[i - 2]
414            && k.close[i] < k.close[i - 2]
415        {
416            -100
417        } else {
418            0
419        }
420    })
421}
422
423/// Three black crows: three long black candles with very short lower shadows,
424/// each opening within the prior body and closing progressively lower.
425pub fn cdl_3blackcrows(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
426    let k = Candles::new(open, high, low, close);
427    scan(k.len, SHADOW_VERY_SHORT.avg_period + 3, |i| {
428        if k.color(i - 3) == 1
429            && k.color(i - 2) == -1
430            && k.lower_shadow(i - 2) < k.average(&SHADOW_VERY_SHORT, i - 2)
431            && k.color(i - 1) == -1
432            && k.lower_shadow(i - 1) < k.average(&SHADOW_VERY_SHORT, i - 1)
433            && k.color(i) == -1
434            && k.lower_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
435            && k.open[i - 1] < k.open[i - 2]
436            && k.open[i - 1] > k.close[i - 2]
437            && k.open[i] < k.open[i - 1]
438            && k.open[i] > k.close[i - 1]
439            && k.high[i - 3] > k.close[i - 2]
440            && k.close[i - 2] > k.close[i - 1]
441            && k.close[i - 1] > k.close[i]
442        {
443            -100
444        } else {
445            0
446        }
447    })
448}
449
450/// Three inside up/down: a long candle, a harami body engulfed by it, then a
451/// confirming candle of the opposite color to the first.
452pub fn cdl_3inside(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
453    let k = Candles::new(open, high, low, close);
454    scan(
455        k.len,
456        BODY_SHORT.avg_period.max(BODY_LONG.avg_period) + 2,
457        |i| {
458            if k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
459                && k.real_body(i - 1) <= k.average(&BODY_SHORT, i - 1)
460                && k.body_top(i - 1) < k.body_top(i - 2)
461                && k.body_bottom(i - 1) > k.body_bottom(i - 2)
462                && ((k.color(i - 2) == 1 && k.color(i) == -1 && k.close[i] < k.open[i - 2])
463                    || (k.color(i - 2) == -1 && k.color(i) == 1 && k.close[i] > k.open[i - 2]))
464            {
465                -k.color(i - 2) * 100
466            } else {
467                0
468            }
469        },
470    )
471}
472
473/// Three-line strike: three candles of one color, then a fourth of the opposite
474/// color that engulfs the prior run.
475pub fn cdl_3linestrike(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
476    let k = Candles::new(open, high, low, close);
477    scan(k.len, NEAR.avg_period + 3, |i| {
478        if k.color(i - 3) == k.color(i - 2)
479            && k.color(i - 2) == k.color(i - 1)
480            && k.color(i) == -k.color(i - 1)
481            && k.open[i - 2] >= k.body_bottom(i - 3) - k.average(&NEAR, i - 3)
482            && k.open[i - 2] <= k.body_top(i - 3) + k.average(&NEAR, i - 3)
483            && k.open[i - 1] >= k.body_bottom(i - 2) - k.average(&NEAR, i - 2)
484            && k.open[i - 1] <= k.body_top(i - 2) + k.average(&NEAR, i - 2)
485            && ((k.color(i - 1) == 1
486                && k.close[i - 1] > k.close[i - 2]
487                && k.close[i - 2] > k.close[i - 3]
488                && k.open[i] > k.close[i - 1]
489                && k.close[i] < k.open[i - 3])
490                || (k.color(i - 1) == -1
491                    && k.close[i - 1] < k.close[i - 2]
492                    && k.close[i - 2] < k.close[i - 3]
493                    && k.open[i] < k.close[i - 1]
494                    && k.close[i] > k.open[i - 3]))
495        {
496            k.color(i - 1) * 100
497        } else {
498            0
499        }
500    })
501}
502
503/// Three outside up/down: an engulfing pair confirmed by a third candle that
504/// continues in the engulfing direction.
505pub fn cdl_3outside(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
506    let k = Candles::new(open, high, low, close);
507    scan(k.len, 3, |i| {
508        if (k.color(i - 1) == 1
509            && k.color(i - 2) == -1
510            && k.close[i - 1] > k.open[i - 2]
511            && k.open[i - 1] < k.close[i - 2]
512            && k.close[i] > k.close[i - 1])
513            || (k.color(i - 1) == -1
514                && k.color(i - 2) == 1
515                && k.open[i - 1] > k.close[i - 2]
516                && k.close[i - 1] < k.open[i - 2]
517                && k.close[i] < k.close[i - 1])
518        {
519            k.color(i - 1) * 100
520        } else {
521            0
522        }
523    })
524}
525
526/// Three stars in the south: a faltering downtrend of three black candles.
527pub fn cdl_3starsinsouth(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
528    let k = Candles::new(open, high, low, close);
529    let lookback = SHADOW_VERY_SHORT
530        .avg_period
531        .max(SHADOW_LONG.avg_period)
532        .max(BODY_LONG.avg_period)
533        .max(BODY_SHORT.avg_period)
534        + 2;
535    scan(k.len, lookback, |i| {
536        if k.color(i - 2) == -1
537            && k.color(i - 1) == -1
538            && k.color(i) == -1
539            && k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
540            && k.lower_shadow(i - 2) > k.average(&SHADOW_LONG, i - 2)
541            && k.real_body(i - 1) < k.real_body(i - 2)
542            && k.open[i - 1] > k.close[i - 2]
543            && k.open[i - 1] <= k.high[i - 2]
544            && k.low[i - 1] < k.close[i - 2]
545            && k.low[i - 1] >= k.low[i - 2]
546            && k.lower_shadow(i - 1) > k.average(&SHADOW_VERY_SHORT, i - 1)
547            && k.real_body(i) < k.average(&BODY_SHORT, i)
548            && k.lower_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
549            && k.upper_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
550            && k.low[i] > k.low[i - 1]
551            && k.high[i] < k.high[i - 1]
552        {
553            100
554        } else {
555            0
556        }
557    })
558}
559
560/// Three white soldiers: three rising white candles with very short upper
561/// shadows and only modest body shrinkage.
562pub fn cdl_3whitesoldiers(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
563    let k = Candles::new(open, high, low, close);
564    let lookback = SHADOW_VERY_SHORT
565        .avg_period
566        .max(BODY_SHORT.avg_period)
567        .max(FAR.avg_period)
568        .max(NEAR.avg_period)
569        + 2;
570    scan(k.len, lookback, |i| {
571        if k.color(i - 2) == 1
572            && k.upper_shadow(i - 2) < k.average(&SHADOW_VERY_SHORT, i - 2)
573            && k.color(i - 1) == 1
574            && k.upper_shadow(i - 1) < k.average(&SHADOW_VERY_SHORT, i - 1)
575            && k.color(i) == 1
576            && k.upper_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
577            && k.close[i] > k.close[i - 1]
578            && k.close[i - 1] > k.close[i - 2]
579            && k.open[i - 1] > k.open[i - 2]
580            && k.open[i - 1] <= k.close[i - 2] + k.average(&NEAR, i - 2)
581            && k.open[i] > k.open[i - 1]
582            && k.open[i] <= k.close[i - 1] + k.average(&NEAR, i - 1)
583            && k.real_body(i - 1) > k.real_body(i - 2) - k.average(&FAR, i - 2)
584            && k.real_body(i) > k.real_body(i - 1) - k.average(&FAR, i - 1)
585            && k.real_body(i) > k.average(&BODY_SHORT, i)
586        {
587            100
588        } else {
589            0
590        }
591    })
592}
593
594/// Abandoned baby: a doji isolated by gaps from a long candle and a confirming
595/// candle of the opposite color.
596pub fn cdl_abandonedbaby(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
597    let k = Candles::new(open, high, low, close);
598    let penetration = 0.3;
599    let lookback = BODY_DOJI
600        .avg_period
601        .max(BODY_LONG.avg_period)
602        .max(BODY_SHORT.avg_period)
603        + 2;
604    scan(k.len, lookback, |i| {
605        if k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
606            && k.real_body(i - 1) <= k.average(&BODY_DOJI, i - 1)
607            && k.real_body(i) > k.average(&BODY_SHORT, i)
608            && ((k.color(i - 2) == 1
609                && k.color(i) == -1
610                && k.close[i] < k.close[i - 2] - k.real_body(i - 2) * penetration
611                && k.candle_gap_up(i - 1, i - 2)
612                && k.candle_gap_down(i, i - 1))
613                || (k.color(i - 2) == -1
614                    && k.color(i) == 1
615                    && k.close[i] > k.close[i - 2] + k.real_body(i - 2) * penetration
616                    && k.candle_gap_down(i - 1, i - 2)
617                    && k.candle_gap_up(i, i - 1)))
618        {
619            k.color(i) * 100
620        } else {
621            0
622        }
623    })
624}
625
626/// Advance block: three rising white candles whose advance falters via
627/// shrinking bodies and lengthening upper shadows.
628pub fn cdl_advanceblock(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
629    let k = Candles::new(open, high, low, close);
630    let lookback = SHADOW_LONG
631        .avg_period
632        .max(SHADOW_SHORT.avg_period)
633        .max(FAR.avg_period)
634        .max(NEAR.avg_period)
635        .max(BODY_LONG.avg_period)
636        + 2;
637    scan(k.len, lookback, |i| {
638        if k.color(i - 2) == 1
639            && k.color(i - 1) == 1
640            && k.color(i) == 1
641            && k.close[i] > k.close[i - 1]
642            && k.close[i - 1] > k.close[i - 2]
643            && k.open[i - 1] > k.open[i - 2]
644            && k.open[i - 1] <= k.close[i - 2] + k.average(&NEAR, i - 2)
645            && k.open[i] > k.open[i - 1]
646            && k.open[i] <= k.close[i - 1] + k.average(&NEAR, i - 1)
647            && k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
648            && k.upper_shadow(i - 2) < k.average(&SHADOW_SHORT, i - 2)
649            && ((k.real_body(i - 1) < k.real_body(i - 2) - k.average(&FAR, i - 2)
650                && k.real_body(i) < k.real_body(i - 1) + k.average(&NEAR, i - 1))
651                || (k.real_body(i) < k.real_body(i - 1) - k.average(&FAR, i - 1))
652                || (k.real_body(i) < k.real_body(i - 1)
653                    && k.real_body(i - 1) < k.real_body(i - 2)
654                    && (k.upper_shadow(i) > k.average(&SHADOW_SHORT, i)
655                        || k.upper_shadow(i - 1) > k.average(&SHADOW_SHORT, i - 1)))
656                || (k.real_body(i) < k.real_body(i - 1)
657                    && k.upper_shadow(i) > k.average(&SHADOW_LONG, i)))
658        {
659            -100
660        } else {
661            0
662        }
663    })
664}
665
666/// Belt hold: a long candle that opens at its extreme (no shadow on the open
667/// end).
668pub fn cdl_belthold(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
669    let k = Candles::new(open, high, low, close);
670    scan(
671        k.len,
672        BODY_LONG.avg_period.max(SHADOW_VERY_SHORT.avg_period),
673        |i| {
674            if k.real_body(i) > k.average(&BODY_LONG, i)
675                && ((k.color(i) == 1 && k.lower_shadow(i) < k.average(&SHADOW_VERY_SHORT, i))
676                    || (k.color(i) == -1 && k.upper_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)))
677            {
678                k.color(i) * 100
679            } else {
680                0
681            }
682        },
683    )
684}
685
686/// Breakaway: a five-candle gap-and-run that closes back through the gap.
687pub fn cdl_breakaway(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
688    let k = Candles::new(open, high, low, close);
689    scan(k.len, BODY_LONG.avg_period + 4, |i| {
690        if k.real_body(i - 4) > k.average(&BODY_LONG, i - 4)
691            && k.color(i - 4) == k.color(i - 3)
692            && k.color(i - 3) == k.color(i - 1)
693            && k.color(i - 1) == -k.color(i)
694            && ((k.color(i - 4) == -1
695                && k.real_body_gap_down(i - 3, i - 4)
696                && k.high[i - 2] < k.high[i - 3]
697                && k.low[i - 2] < k.low[i - 3]
698                && k.high[i - 1] < k.high[i - 2]
699                && k.low[i - 1] < k.low[i - 2]
700                && k.close[i] > k.open[i - 3]
701                && k.close[i] < k.close[i - 4])
702                || (k.color(i - 4) == 1
703                    && k.real_body_gap_up(i - 3, i - 4)
704                    && k.high[i - 2] > k.high[i - 3]
705                    && k.low[i - 2] > k.low[i - 3]
706                    && k.high[i - 1] > k.high[i - 2]
707                    && k.low[i - 1] > k.low[i - 2]
708                    && k.close[i] < k.open[i - 3]
709                    && k.close[i] > k.close[i - 4]))
710        {
711            k.color(i) * 100
712        } else {
713            0
714        }
715    })
716}
717
718/// Concealing baby swallow: four black candles where the third's upper shadow
719/// pierces the second body and the fourth engulfs the third.
720pub fn cdl_concealbabyswall(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
721    let k = Candles::new(open, high, low, close);
722    scan(k.len, SHADOW_VERY_SHORT.avg_period + 3, |i| {
723        if k.color(i - 3) == -1
724            && k.color(i - 2) == -1
725            && k.color(i - 1) == -1
726            && k.color(i) == -1
727            && k.lower_shadow(i - 3) < k.average(&SHADOW_VERY_SHORT, i - 3)
728            && k.upper_shadow(i - 3) < k.average(&SHADOW_VERY_SHORT, i - 3)
729            && k.lower_shadow(i - 2) < k.average(&SHADOW_VERY_SHORT, i - 2)
730            && k.upper_shadow(i - 2) < k.average(&SHADOW_VERY_SHORT, i - 2)
731            && k.real_body_gap_down(i - 1, i - 2)
732            && k.upper_shadow(i - 1) > k.average(&SHADOW_VERY_SHORT, i - 1)
733            && k.high[i - 1] > k.close[i - 2]
734            && k.high[i] > k.high[i - 1]
735            && k.low[i] < k.low[i - 1]
736        {
737            100
738        } else {
739            0
740        }
741    })
742}
743
744/// Counterattack: two long opposite-colored candles that close at the same
745/// level.
746pub fn cdl_counterattack(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
747    let k = Candles::new(open, high, low, close);
748    scan(k.len, EQUAL.avg_period.max(BODY_LONG.avg_period) + 1, |i| {
749        if k.color(i - 1) == -k.color(i)
750            && k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
751            && k.real_body(i) > k.average(&BODY_LONG, i)
752            && k.close[i] <= k.close[i - 1] + k.average(&EQUAL, i - 1)
753            && k.close[i] >= k.close[i - 1] - k.average(&EQUAL, i - 1)
754        {
755            k.color(i) * 100
756        } else {
757            0
758        }
759    })
760}
761
762/// Dark cloud cover: a black candle opening above a prior long white candle and
763/// closing well into its body.
764pub fn cdl_darkcloudcover(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
765    let k = Candles::new(open, high, low, close);
766    let penetration = 0.5;
767    scan(k.len, BODY_LONG.avg_period + 1, |i| {
768        if k.color(i - 1) == 1
769            && k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
770            && k.color(i) == -1
771            && k.open[i] > k.high[i - 1]
772            && k.close[i] > k.open[i - 1]
773            && k.close[i] < k.close[i - 1] - k.real_body(i - 1) * penetration
774        {
775            -100
776        } else {
777            0
778        }
779    })
780}
781
782/// Doji star: a long candle followed by a gapping doji.
783pub fn cdl_dojistar(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
784    let k = Candles::new(open, high, low, close);
785    scan(
786        k.len,
787        BODY_DOJI.avg_period.max(BODY_LONG.avg_period) + 1,
788        |i| {
789            if k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
790                && k.real_body(i) <= k.average(&BODY_DOJI, i)
791                && ((k.color(i - 1) == 1 && k.real_body_gap_up(i, i - 1))
792                    || (k.color(i - 1) == -1 && k.real_body_gap_down(i, i - 1)))
793            {
794                -k.color(i - 1) * 100
795            } else {
796                0
797            }
798        },
799    )
800}
801
802/// Engulfing: a candle whose real body fully engulfs the prior opposite-colored
803/// body (±80 when one end exactly matches).
804pub fn cdl_engulfing(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
805    let k = Candles::new(open, high, low, close);
806    scan(k.len, 2, |i| {
807        let engulfs = (k.color(i) == 1
808            && k.color(i - 1) == -1
809            && ((k.close[i] >= k.open[i - 1] && k.open[i] < k.close[i - 1])
810                || (k.close[i] > k.open[i - 1] && k.open[i] <= k.close[i - 1])))
811            || (k.color(i) == -1
812                && k.color(i - 1) == 1
813                && ((k.open[i] >= k.close[i - 1] && k.close[i] < k.open[i - 1])
814                    || (k.open[i] > k.close[i - 1] && k.close[i] <= k.open[i - 1])));
815        if engulfs {
816            if k.open[i] != k.close[i - 1] && k.close[i] != k.open[i - 1] {
817                k.color(i) * 100
818            } else {
819                k.color(i) * 80
820            }
821        } else {
822            0
823        }
824    })
825}
826
827/// Evening doji star: long white, gapping doji, then a black candle closing well
828/// into the first body.
829pub fn cdl_eveningdojistar(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
830    let k = Candles::new(open, high, low, close);
831    let penetration = 0.3;
832    let lookback = BODY_DOJI
833        .avg_period
834        .max(BODY_LONG.avg_period)
835        .max(BODY_SHORT.avg_period)
836        + 2;
837    scan(k.len, lookback, |i| {
838        if k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
839            && k.color(i - 2) == 1
840            && k.real_body(i - 1) <= k.average(&BODY_DOJI, i - 1)
841            && k.real_body_gap_up(i - 1, i - 2)
842            && k.real_body(i) > k.average(&BODY_SHORT, i)
843            && k.color(i) == -1
844            && k.close[i] < k.close[i - 2] - k.real_body(i - 2) * penetration
845        {
846            -100
847        } else {
848            0
849        }
850    })
851}
852
853/// Evening star: long white, a short-bodied star gapping up, then a black candle
854/// closing well into the first body.
855pub fn cdl_eveningstar(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
856    let k = Candles::new(open, high, low, close);
857    let penetration = 0.3;
858    scan(
859        k.len,
860        BODY_SHORT.avg_period.max(BODY_LONG.avg_period) + 2,
861        |i| {
862            if k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
863                && k.color(i - 2) == 1
864                && k.real_body(i - 1) <= k.average(&BODY_SHORT, i - 1)
865                && k.real_body_gap_up(i - 1, i - 2)
866                && k.real_body(i) > k.average(&BODY_SHORT, i)
867                && k.color(i) == -1
868                && k.close[i] < k.close[i - 2] - k.real_body(i - 2) * penetration
869            {
870                -100
871            } else {
872                0
873            }
874        },
875    )
876}
877
878/// Up/down-gap side-by-side white lines: a gap followed by two similar white
879/// candles.
880pub fn cdl_gapsidesidewhite(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
881    let k = Candles::new(open, high, low, close);
882    scan(k.len, NEAR.avg_period.max(EQUAL.avg_period) + 2, |i| {
883        let gap_up = k.real_body_gap_up(i - 1, i - 2) && k.real_body_gap_up(i, i - 2);
884        let gap_down = k.real_body_gap_down(i - 1, i - 2) && k.real_body_gap_down(i, i - 2);
885        if (gap_up || gap_down)
886            && k.color(i - 1) == 1
887            && k.color(i) == 1
888            && k.real_body(i) >= k.real_body(i - 1) - k.average(&NEAR, i - 1)
889            && k.real_body(i) <= k.real_body(i - 1) + k.average(&NEAR, i - 1)
890            && k.open[i] >= k.open[i - 1] - k.average(&EQUAL, i - 1)
891            && k.open[i] <= k.open[i - 1] + k.average(&EQUAL, i - 1)
892        {
893            if gap_up { 100 } else { -100 }
894        } else {
895            0
896        }
897    })
898}
899
900/// Hammer: a small-bodied candle with a long lower shadow near the prior lows.
901pub fn cdl_hammer(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
902    let k = Candles::new(open, high, low, close);
903    let lookback = BODY_SHORT
904        .avg_period
905        .max(SHADOW_LONG.avg_period)
906        .max(SHADOW_VERY_SHORT.avg_period)
907        .max(NEAR.avg_period)
908        + 1;
909    scan(k.len, lookback, |i| {
910        if k.real_body(i) < k.average(&BODY_SHORT, i)
911            && k.lower_shadow(i) > k.average(&SHADOW_LONG, i)
912            && k.upper_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
913            && k.body_bottom(i) <= k.low[i - 1] + k.average(&NEAR, i - 1)
914        {
915            100
916        } else {
917            0
918        }
919    })
920}
921
922/// Hanging man: a small-bodied candle with a long lower shadow near the prior
923/// highs.
924pub fn cdl_hangingman(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
925    let k = Candles::new(open, high, low, close);
926    let lookback = BODY_SHORT
927        .avg_period
928        .max(SHADOW_LONG.avg_period)
929        .max(SHADOW_VERY_SHORT.avg_period)
930        .max(NEAR.avg_period)
931        + 1;
932    scan(k.len, lookback, |i| {
933        if k.real_body(i) < k.average(&BODY_SHORT, i)
934            && k.lower_shadow(i) > k.average(&SHADOW_LONG, i)
935            && k.upper_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
936            && k.body_bottom(i) >= k.high[i - 1] - k.average(&NEAR, i - 1)
937        {
938            -100
939        } else {
940            0
941        }
942    })
943}
944
945/// Harami: a small real body engulfed by the prior long body (±80 when one end
946/// matches).
947pub fn cdl_harami(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
948    let k = Candles::new(open, high, low, close);
949    scan(
950        k.len,
951        BODY_SHORT.avg_period.max(BODY_LONG.avg_period) + 1,
952        |i| {
953            if k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
954                && k.real_body(i) <= k.average(&BODY_SHORT, i)
955            {
956                if k.body_top(i) < k.body_top(i - 1) && k.body_bottom(i) > k.body_bottom(i - 1) {
957                    -k.color(i - 1) * 100
958                } else if k.body_top(i) <= k.body_top(i - 1)
959                    && k.body_bottom(i) >= k.body_bottom(i - 1)
960                {
961                    -k.color(i - 1) * 80
962                } else {
963                    0
964                }
965            } else {
966                0
967            }
968        },
969    )
970}
971
972/// Harami cross: a harami whose inside candle is a doji.
973pub fn cdl_haramicross(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
974    let k = Candles::new(open, high, low, close);
975    scan(
976        k.len,
977        BODY_DOJI.avg_period.max(BODY_LONG.avg_period) + 1,
978        |i| {
979            if k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
980                && k.real_body(i) <= k.average(&BODY_DOJI, i)
981            {
982                if k.body_top(i) < k.body_top(i - 1) && k.body_bottom(i) > k.body_bottom(i - 1) {
983                    -k.color(i - 1) * 100
984                } else if k.body_top(i) <= k.body_top(i - 1)
985                    && k.body_bottom(i) >= k.body_bottom(i - 1)
986                {
987                    -k.color(i - 1) * 80
988                } else {
989                    0
990                }
991            } else {
992                0
993            }
994        },
995    )
996}
997
998/// Hikkake: an inside bar followed by a false breakout, optionally confirmed
999/// within three bars (±200 on confirmation). Stateful across bars.
1000pub fn cdl_hikkake(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1001    let k = Candles::new(open, high, low, close);
1002    let lookback = 5usize;
1003    let mut out = vec![0i32; k.len];
1004    if lookback >= k.len {
1005        return out;
1006    }
1007    let detect = |i: usize| -> bool {
1008        k.high[i - 1] < k.high[i - 2]
1009            && k.low[i - 1] > k.low[i - 2]
1010            && ((k.high[i] < k.high[i - 1] && k.low[i] < k.low[i - 1])
1011                || (k.high[i] > k.high[i - 1] && k.low[i] > k.low[i - 1]))
1012    };
1013    let mut pattern_result = 0i32;
1014    let mut pattern_idx = 0usize;
1015    let mut i = lookback - 3;
1016    while i < k.len {
1017        if detect(i) {
1018            pattern_result = 100 * if k.high[i] < k.high[i - 1] { 1 } else { -1 };
1019            pattern_idx = i;
1020            if i >= lookback {
1021                out[i] = pattern_result;
1022            }
1023        } else if i <= pattern_idx + 3
1024            && ((pattern_result > 0 && k.close[i] > k.high[pattern_idx - 1])
1025                || (pattern_result < 0 && k.close[i] < k.low[pattern_idx - 1]))
1026        {
1027            if i >= lookback {
1028                out[i] = pattern_result + 100 * if pattern_result > 0 { 1 } else { -1 };
1029            }
1030            pattern_idx = 0;
1031        }
1032        i += 1;
1033    }
1034    out
1035}
1036
1037/// Modified hikkake: a three-bar inside formation with a close near the second
1038/// candle's extreme, optionally confirmed within three bars. Stateful.
1039pub fn cdl_hikkakemod(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1040    let k = Candles::new(open, high, low, close);
1041    let lookback = 1usize.max(NEAR.avg_period) + 5;
1042    let mut out = vec![0i32; k.len];
1043    if lookback >= k.len {
1044        return out;
1045    }
1046    let detect = |i: usize| -> bool {
1047        let near = k.average(&NEAR, i - 2);
1048        k.high[i - 2] < k.high[i - 3]
1049            && k.low[i - 2] > k.low[i - 3]
1050            && k.high[i - 1] < k.high[i - 2]
1051            && k.low[i - 1] > k.low[i - 2]
1052            && ((k.high[i] < k.high[i - 1]
1053                && k.low[i] < k.low[i - 1]
1054                && k.close[i - 2] <= k.low[i - 2] + near)
1055                || (k.high[i] > k.high[i - 1]
1056                    && k.low[i] > k.low[i - 1]
1057                    && k.close[i - 2] >= k.high[i - 2] - near))
1058    };
1059    let mut pattern_result = 0i32;
1060    let mut pattern_idx = 0usize;
1061    let mut i = lookback - 3;
1062    while i < k.len {
1063        if detect(i) {
1064            pattern_result = 100 * if k.high[i] < k.high[i - 1] { 1 } else { -1 };
1065            pattern_idx = i;
1066            if i >= lookback {
1067                out[i] = pattern_result;
1068            }
1069        } else if i <= pattern_idx + 3
1070            && ((pattern_result > 0 && k.close[i] > k.high[pattern_idx - 1])
1071                || (pattern_result < 0 && k.close[i] < k.low[pattern_idx - 1]))
1072        {
1073            if i >= lookback {
1074                out[i] = pattern_result + 100 * if pattern_result > 0 { 1 } else { -1 };
1075            }
1076            pattern_idx = 0;
1077        }
1078        i += 1;
1079    }
1080    out
1081}
1082
1083/// Homing pigeon: a small black body engulfed by a prior long black body.
1084pub fn cdl_homingpigeon(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1085    let k = Candles::new(open, high, low, close);
1086    scan(
1087        k.len,
1088        BODY_SHORT.avg_period.max(BODY_LONG.avg_period) + 1,
1089        |i| {
1090            if k.color(i - 1) == -1
1091                && k.color(i) == -1
1092                && k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
1093                && k.real_body(i) <= k.average(&BODY_SHORT, i)
1094                && k.open[i] < k.open[i - 1]
1095                && k.close[i] > k.close[i - 1]
1096            {
1097                100
1098            } else {
1099                0
1100            }
1101        },
1102    )
1103}
1104
1105/// Identical three crows: three black candles whose opens match the prior
1106/// closes.
1107pub fn cdl_identical3crows(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1108    let k = Candles::new(open, high, low, close);
1109    scan(
1110        k.len,
1111        SHADOW_VERY_SHORT.avg_period.max(EQUAL.avg_period) + 2,
1112        |i| {
1113            if k.color(i - 2) == -1
1114                && k.lower_shadow(i - 2) < k.average(&SHADOW_VERY_SHORT, i - 2)
1115                && k.color(i - 1) == -1
1116                && k.lower_shadow(i - 1) < k.average(&SHADOW_VERY_SHORT, i - 1)
1117                && k.color(i) == -1
1118                && k.lower_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
1119                && k.close[i - 2] > k.close[i - 1]
1120                && k.close[i - 1] > k.close[i]
1121                && k.open[i - 1] <= k.close[i - 2] + k.average(&EQUAL, i - 2)
1122                && k.open[i - 1] >= k.close[i - 2] - k.average(&EQUAL, i - 2)
1123                && k.open[i] <= k.close[i - 1] + k.average(&EQUAL, i - 1)
1124                && k.open[i] >= k.close[i - 1] - k.average(&EQUAL, i - 1)
1125            {
1126                -100
1127            } else {
1128                0
1129            }
1130        },
1131    )
1132}
1133
1134/// In-neck: a black candle followed by a white one closing just into the prior
1135/// body.
1136pub fn cdl_inneck(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1137    let k = Candles::new(open, high, low, close);
1138    scan(k.len, EQUAL.avg_period.max(BODY_LONG.avg_period) + 1, |i| {
1139        if k.color(i - 1) == -1
1140            && k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
1141            && k.color(i) == 1
1142            && k.open[i] < k.low[i - 1]
1143            && k.close[i] <= k.close[i - 1] + k.average(&EQUAL, i - 1)
1144            && k.close[i] >= k.close[i - 1]
1145        {
1146            -100
1147        } else {
1148            0
1149        }
1150    })
1151}
1152
1153/// Inverted hammer: a small-bodied candle with a long upper shadow gapping down.
1154pub fn cdl_invertedhammer(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1155    let k = Candles::new(open, high, low, close);
1156    let lookback = BODY_SHORT
1157        .avg_period
1158        .max(SHADOW_LONG.avg_period)
1159        .max(SHADOW_VERY_SHORT.avg_period)
1160        + 1;
1161    scan(k.len, lookback, |i| {
1162        if k.real_body(i) < k.average(&BODY_SHORT, i)
1163            && k.upper_shadow(i) > k.average(&SHADOW_LONG, i)
1164            && k.lower_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
1165            && k.real_body_gap_down(i, i - 1)
1166        {
1167            100
1168        } else {
1169            0
1170        }
1171    })
1172}
1173
1174/// Kicking: two opposite marubozu separated by a gap.
1175pub fn cdl_kicking(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1176    let k = Candles::new(open, high, low, close);
1177    scan(
1178        k.len,
1179        SHADOW_VERY_SHORT.avg_period.max(BODY_LONG.avg_period) + 1,
1180        |i| {
1181            if k.color(i - 1) == -k.color(i)
1182                && k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
1183                && k.upper_shadow(i - 1) < k.average(&SHADOW_VERY_SHORT, i - 1)
1184                && k.lower_shadow(i - 1) < k.average(&SHADOW_VERY_SHORT, i - 1)
1185                && k.real_body(i) > k.average(&BODY_LONG, i)
1186                && k.upper_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
1187                && k.lower_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
1188                && ((k.color(i - 1) == -1 && k.candle_gap_up(i, i - 1))
1189                    || (k.color(i - 1) == 1 && k.candle_gap_down(i, i - 1)))
1190            {
1191                k.color(i) * 100
1192            } else {
1193                0
1194            }
1195        },
1196    )
1197}
1198
1199/// Kicking by length: like kicking, but the result follows the longer marubozu.
1200pub fn cdl_kickingbylength(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1201    let k = Candles::new(open, high, low, close);
1202    scan(
1203        k.len,
1204        SHADOW_VERY_SHORT.avg_period.max(BODY_LONG.avg_period) + 1,
1205        |i| {
1206            if k.color(i - 1) == -k.color(i)
1207                && k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
1208                && k.upper_shadow(i - 1) < k.average(&SHADOW_VERY_SHORT, i - 1)
1209                && k.lower_shadow(i - 1) < k.average(&SHADOW_VERY_SHORT, i - 1)
1210                && k.real_body(i) > k.average(&BODY_LONG, i)
1211                && k.upper_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
1212                && k.lower_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
1213                && ((k.color(i - 1) == -1 && k.candle_gap_up(i, i - 1))
1214                    || (k.color(i - 1) == 1 && k.candle_gap_down(i, i - 1)))
1215            {
1216                let longer = if k.real_body(i) > k.real_body(i - 1) {
1217                    i
1218                } else {
1219                    i - 1
1220                };
1221                k.color(longer) * 100
1222            } else {
1223                0
1224            }
1225        },
1226    )
1227}
1228
1229/// Ladder bottom: three declining black candles, a black candle with an upper
1230/// shadow, then a white candle breaking out.
1231pub fn cdl_ladderbottom(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1232    let k = Candles::new(open, high, low, close);
1233    scan(k.len, SHADOW_VERY_SHORT.avg_period + 4, |i| {
1234        if k.color(i - 4) == -1
1235            && k.color(i - 3) == -1
1236            && k.color(i - 2) == -1
1237            && k.open[i - 4] > k.open[i - 3]
1238            && k.open[i - 3] > k.open[i - 2]
1239            && k.close[i - 4] > k.close[i - 3]
1240            && k.close[i - 3] > k.close[i - 2]
1241            && k.color(i - 1) == -1
1242            && k.upper_shadow(i - 1) > k.average(&SHADOW_VERY_SHORT, i - 1)
1243            && k.color(i) == 1
1244            && k.open[i] > k.open[i - 1]
1245            && k.close[i] > k.high[i - 1]
1246        {
1247            100
1248        } else {
1249            0
1250        }
1251    })
1252}
1253
1254/// Matching low: two black candles closing at the same level.
1255pub fn cdl_matchinglow(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1256    let k = Candles::new(open, high, low, close);
1257    scan(k.len, EQUAL.avg_period + 1, |i| {
1258        if k.color(i - 1) == -1
1259            && k.color(i) == -1
1260            && k.close[i] <= k.close[i - 1] + k.average(&EQUAL, i - 1)
1261            && k.close[i] >= k.close[i - 1] - k.average(&EQUAL, i - 1)
1262        {
1263            100
1264        } else {
1265            0
1266        }
1267    })
1268}
1269
1270/// Mat hold: a long white candle, an upside gap, three holding candles, then a
1271/// white candle breaking out.
1272pub fn cdl_mathold(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1273    let k = Candles::new(open, high, low, close);
1274    let penetration = 0.5;
1275    scan(
1276        k.len,
1277        BODY_SHORT.avg_period.max(BODY_LONG.avg_period) + 4,
1278        |i| {
1279            if k.real_body(i - 4) > k.average(&BODY_LONG, i - 4)
1280                && k.real_body(i - 3) < k.average(&BODY_SHORT, i - 3)
1281                && k.real_body(i - 2) < k.average(&BODY_SHORT, i - 2)
1282                && k.real_body(i - 1) < k.average(&BODY_SHORT, i - 1)
1283                && k.color(i - 4) == 1
1284                && k.color(i - 3) == -1
1285                && k.color(i) == 1
1286                && k.real_body_gap_up(i - 3, i - 4)
1287                && k.body_bottom(i - 2) < k.close[i - 4]
1288                && k.body_bottom(i - 1) < k.close[i - 4]
1289                && k.body_bottom(i - 2) > k.close[i - 4] - k.real_body(i - 4) * penetration
1290                && k.body_bottom(i - 1) > k.close[i - 4] - k.real_body(i - 4) * penetration
1291                && k.body_top(i - 2) < k.open[i - 3]
1292                && k.body_top(i - 1) < k.body_top(i - 2)
1293                && k.open[i] > k.close[i - 1]
1294                && k.close[i] > k.high[i - 3].max(k.high[i - 2]).max(k.high[i - 1])
1295            {
1296                100
1297            } else {
1298                0
1299            }
1300        },
1301    )
1302}
1303
1304/// Morning doji star: long black, gapping doji, then a white candle closing well
1305/// into the first body.
1306pub fn cdl_morningdojistar(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1307    let k = Candles::new(open, high, low, close);
1308    let penetration = 0.3;
1309    let lookback = BODY_DOJI
1310        .avg_period
1311        .max(BODY_LONG.avg_period)
1312        .max(BODY_SHORT.avg_period)
1313        + 2;
1314    scan(k.len, lookback, |i| {
1315        if k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
1316            && k.color(i - 2) == -1
1317            && k.real_body(i - 1) <= k.average(&BODY_DOJI, i - 1)
1318            && k.real_body_gap_down(i - 1, i - 2)
1319            && k.real_body(i) > k.average(&BODY_SHORT, i)
1320            && k.color(i) == 1
1321            && k.close[i] > k.close[i - 2] + k.real_body(i - 2) * penetration
1322        {
1323            100
1324        } else {
1325            0
1326        }
1327    })
1328}
1329
1330/// Morning star: long black, a short-bodied star gapping down, then a white
1331/// candle closing well into the first body.
1332pub fn cdl_morningstar(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1333    let k = Candles::new(open, high, low, close);
1334    let penetration = 0.3;
1335    scan(
1336        k.len,
1337        BODY_SHORT.avg_period.max(BODY_LONG.avg_period) + 2,
1338        |i| {
1339            if k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
1340                && k.color(i - 2) == -1
1341                && k.real_body(i - 1) <= k.average(&BODY_SHORT, i - 1)
1342                && k.real_body_gap_down(i - 1, i - 2)
1343                && k.real_body(i) > k.average(&BODY_SHORT, i)
1344                && k.color(i) == 1
1345                && k.close[i] > k.close[i - 2] + k.real_body(i - 2) * penetration
1346            {
1347                100
1348            } else {
1349                0
1350            }
1351        },
1352    )
1353}
1354
1355/// On-neck: a black candle followed by a white one closing at the prior low.
1356pub fn cdl_onneck(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1357    let k = Candles::new(open, high, low, close);
1358    scan(k.len, EQUAL.avg_period.max(BODY_LONG.avg_period) + 1, |i| {
1359        if k.color(i - 1) == -1
1360            && k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
1361            && k.color(i) == 1
1362            && k.open[i] < k.low[i - 1]
1363            && k.close[i] <= k.low[i - 1] + k.average(&EQUAL, i - 1)
1364            && k.close[i] >= k.low[i - 1] - k.average(&EQUAL, i - 1)
1365        {
1366            -100
1367        } else {
1368            0
1369        }
1370    })
1371}
1372
1373/// Piercing: a black candle followed by a white candle opening below the prior
1374/// low and closing above the prior midpoint.
1375pub fn cdl_piercing(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1376    let k = Candles::new(open, high, low, close);
1377    scan(k.len, BODY_LONG.avg_period + 1, |i| {
1378        if k.color(i - 1) == -1
1379            && k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
1380            && k.color(i) == 1
1381            && k.real_body(i) > k.average(&BODY_LONG, i)
1382            && k.open[i] < k.low[i - 1]
1383            && k.close[i] < k.open[i - 1]
1384            && k.close[i] > k.close[i - 1] + k.real_body(i - 1) * 0.5
1385        {
1386            100
1387        } else {
1388            0
1389        }
1390    })
1391}
1392
1393/// Rickshaw man: a long-legged doji whose body sits near the high-low midpoint.
1394pub fn cdl_rickshawman(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1395    let k = Candles::new(open, high, low, close);
1396    let lookback = BODY_DOJI
1397        .avg_period
1398        .max(SHADOW_LONG.avg_period)
1399        .max(NEAR.avg_period);
1400    scan(k.len, lookback, |i| {
1401        let near = k.average(&NEAR, i);
1402        let mid = k.low[i] + k.high_low(i) / 2.0;
1403        if k.real_body(i) <= k.average(&BODY_DOJI, i)
1404            && k.lower_shadow(i) > k.average(&SHADOW_LONG, i)
1405            && k.upper_shadow(i) > k.average(&SHADOW_LONG, i)
1406            && k.body_bottom(i) <= mid + near
1407            && k.body_top(i) >= mid - near
1408        {
1409            100
1410        } else {
1411            0
1412        }
1413    })
1414}
1415
1416/// Rising/falling three methods: a long candle, three small counter-trend
1417/// candles held within its range, then a long continuation candle.
1418pub fn cdl_risefall3methods(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1419    let k = Candles::new(open, high, low, close);
1420    scan(
1421        k.len,
1422        BODY_SHORT.avg_period.max(BODY_LONG.avg_period) + 4,
1423        |i| {
1424            let dir = k.color(i - 4) as f64;
1425            if k.real_body(i - 4) > k.average(&BODY_LONG, i - 4)
1426                && k.real_body(i - 3) < k.average(&BODY_SHORT, i - 3)
1427                && k.real_body(i - 2) < k.average(&BODY_SHORT, i - 2)
1428                && k.real_body(i - 1) < k.average(&BODY_SHORT, i - 1)
1429                && k.real_body(i) > k.average(&BODY_LONG, i)
1430                && k.color(i - 4) == -k.color(i - 3)
1431                && k.color(i - 3) == k.color(i - 2)
1432                && k.color(i - 2) == k.color(i - 1)
1433                && k.color(i - 1) == -k.color(i)
1434                && k.body_bottom(i - 3) < k.high[i - 4]
1435                && k.body_top(i - 3) > k.low[i - 4]
1436                && k.body_bottom(i - 2) < k.high[i - 4]
1437                && k.body_top(i - 2) > k.low[i - 4]
1438                && k.body_bottom(i - 1) < k.high[i - 4]
1439                && k.body_top(i - 1) > k.low[i - 4]
1440                && k.close[i - 2] * dir < k.close[i - 3] * dir
1441                && k.close[i - 1] * dir < k.close[i - 2] * dir
1442                && k.open[i] * dir > k.close[i - 1] * dir
1443                && k.close[i] * dir > k.close[i - 4] * dir
1444            {
1445                100 * k.color(i - 4)
1446            } else {
1447                0
1448            }
1449        },
1450    )
1451}
1452
1453/// Separating lines: a candle that opens at the prior open but reverses into a
1454/// long belt-hold of the opposite color.
1455pub fn cdl_separatinglines(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1456    let k = Candles::new(open, high, low, close);
1457    let lookback = SHADOW_VERY_SHORT
1458        .avg_period
1459        .max(BODY_LONG.avg_period)
1460        .max(EQUAL.avg_period)
1461        + 1;
1462    scan(k.len, lookback, |i| {
1463        if k.color(i - 1) == -k.color(i)
1464            && k.open[i] <= k.open[i - 1] + k.average(&EQUAL, i - 1)
1465            && k.open[i] >= k.open[i - 1] - k.average(&EQUAL, i - 1)
1466            && k.real_body(i) > k.average(&BODY_LONG, i)
1467            && ((k.color(i) == 1 && k.lower_shadow(i) < k.average(&SHADOW_VERY_SHORT, i))
1468                || (k.color(i) == -1 && k.upper_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)))
1469        {
1470            k.color(i) * 100
1471        } else {
1472            0
1473        }
1474    })
1475}
1476
1477/// Shooting star: a small-bodied candle with a long upper shadow gapping up.
1478pub fn cdl_shootingstar(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1479    let k = Candles::new(open, high, low, close);
1480    let lookback = BODY_SHORT
1481        .avg_period
1482        .max(SHADOW_LONG.avg_period)
1483        .max(SHADOW_VERY_SHORT.avg_period)
1484        + 1;
1485    scan(k.len, lookback, |i| {
1486        if k.real_body(i) < k.average(&BODY_SHORT, i)
1487            && k.upper_shadow(i) > k.average(&SHADOW_LONG, i)
1488            && k.lower_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
1489            && k.real_body_gap_up(i, i - 1)
1490        {
1491            -100
1492        } else {
1493            0
1494        }
1495    })
1496}
1497
1498/// Stalled pattern: two long white candles then a small white candle riding the
1499/// second's shoulder.
1500pub fn cdl_stalledpattern(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1501    let k = Candles::new(open, high, low, close);
1502    let lookback = BODY_LONG
1503        .avg_period
1504        .max(BODY_SHORT.avg_period)
1505        .max(SHADOW_VERY_SHORT.avg_period)
1506        .max(NEAR.avg_period)
1507        + 2;
1508    scan(k.len, lookback, |i| {
1509        if k.color(i - 2) == 1
1510            && k.color(i - 1) == 1
1511            && k.color(i) == 1
1512            && k.close[i] > k.close[i - 1]
1513            && k.close[i - 1] > k.close[i - 2]
1514            && k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
1515            && k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
1516            && k.upper_shadow(i - 1) < k.average(&SHADOW_VERY_SHORT, i - 1)
1517            && k.open[i - 1] > k.open[i - 2]
1518            && k.open[i - 1] <= k.close[i - 2] + k.average(&NEAR, i - 2)
1519            && k.real_body(i) < k.average(&BODY_SHORT, i)
1520            && k.open[i] >= k.close[i - 1] - k.real_body(i) - k.average(&NEAR, i - 1)
1521        {
1522            -100
1523        } else {
1524            0
1525        }
1526    })
1527}
1528
1529/// Stick sandwich: a black candle, a white candle trading above it, then a black
1530/// candle closing at the first close.
1531pub fn cdl_sticksandwich(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1532    let k = Candles::new(open, high, low, close);
1533    scan(k.len, EQUAL.avg_period + 2, |i| {
1534        if k.color(i - 2) == -1
1535            && k.color(i - 1) == 1
1536            && k.color(i) == -1
1537            && k.low[i - 1] > k.close[i - 2]
1538            && k.close[i] <= k.close[i - 2] + k.average(&EQUAL, i - 2)
1539            && k.close[i] >= k.close[i - 2] - k.average(&EQUAL, i - 2)
1540        {
1541            100
1542        } else {
1543            0
1544        }
1545    })
1546}
1547
1548/// Takuri: a doji-bodied candle with a very long lower shadow and negligible
1549/// upper shadow.
1550pub fn cdl_takuri(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1551    let k = Candles::new(open, high, low, close);
1552    let lookback = BODY_DOJI
1553        .avg_period
1554        .max(SHADOW_VERY_SHORT.avg_period)
1555        .max(SHADOW_VERY_LONG.avg_period);
1556    scan(k.len, lookback, |i| {
1557        if k.real_body(i) <= k.average(&BODY_DOJI, i)
1558            && k.upper_shadow(i) < k.average(&SHADOW_VERY_SHORT, i)
1559            && k.lower_shadow(i) > k.average(&SHADOW_VERY_LONG, i)
1560        {
1561            100
1562        } else {
1563            0
1564        }
1565    })
1566}
1567
1568/// Tasuki gap: a gap followed by an opposite-colored candle of similar size that
1569/// fails to close the gap.
1570pub fn cdl_tasukigap(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1571    let k = Candles::new(open, high, low, close);
1572    scan(k.len, NEAR.avg_period + 2, |i| {
1573        let near = k.average(&NEAR, i - 1);
1574        if (k.real_body_gap_up(i - 1, i - 2)
1575            && k.color(i - 1) == 1
1576            && k.color(i) == -1
1577            && k.open[i] < k.close[i - 1]
1578            && k.open[i] > k.open[i - 1]
1579            && k.close[i] < k.open[i - 1]
1580            && k.close[i] > k.body_top(i - 2)
1581            && (k.real_body(i - 1) - k.real_body(i)).abs() < near)
1582            || (k.real_body_gap_down(i - 1, i - 2)
1583                && k.color(i - 1) == -1
1584                && k.color(i) == 1
1585                && k.open[i] < k.open[i - 1]
1586                && k.open[i] > k.close[i - 1]
1587                && k.close[i] > k.open[i - 1]
1588                && k.close[i] < k.body_bottom(i - 2)
1589                && (k.real_body(i - 1) - k.real_body(i)).abs() < near)
1590        {
1591            k.color(i - 1) * 100
1592        } else {
1593            0
1594        }
1595    })
1596}
1597
1598/// Thrusting: a black candle followed by a white candle closing into, but under
1599/// the midpoint of, the prior body.
1600pub fn cdl_thrusting(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1601    let k = Candles::new(open, high, low, close);
1602    scan(k.len, EQUAL.avg_period.max(BODY_LONG.avg_period) + 1, |i| {
1603        if k.color(i - 1) == -1
1604            && k.real_body(i - 1) > k.average(&BODY_LONG, i - 1)
1605            && k.color(i) == 1
1606            && k.open[i] < k.low[i - 1]
1607            && k.close[i] > k.close[i - 1] + k.average(&EQUAL, i - 1)
1608            && k.close[i] <= k.close[i - 1] + k.real_body(i - 1) * 0.5
1609        {
1610            -100
1611        } else {
1612            0
1613        }
1614    })
1615}
1616
1617/// Tristar: three doji where the middle one gaps away from the others.
1618pub fn cdl_tristar(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1619    let k = Candles::new(open, high, low, close);
1620    scan(k.len, BODY_DOJI.avg_period + 2, |i| {
1621        let doji = k.average(&BODY_DOJI, i - 2);
1622        if k.real_body(i - 2) <= doji && k.real_body(i - 1) <= doji && k.real_body(i) <= doji {
1623            let mut result = 0;
1624            if k.real_body_gap_up(i - 1, i - 2) && k.body_top(i) < k.body_top(i - 1) {
1625                result = -100;
1626            }
1627            if k.real_body_gap_down(i - 1, i - 2) && k.body_bottom(i) > k.body_bottom(i - 1) {
1628                result = 100;
1629            }
1630            result
1631        } else {
1632            0
1633        }
1634    })
1635}
1636
1637/// Unique three river bottom: a long black candle, a black harami making a new
1638/// low, then a small white candle.
1639pub fn cdl_unique3river(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1640    let k = Candles::new(open, high, low, close);
1641    scan(
1642        k.len,
1643        BODY_SHORT.avg_period.max(BODY_LONG.avg_period) + 2,
1644        |i| {
1645            if k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
1646                && k.color(i - 2) == -1
1647                && k.color(i - 1) == -1
1648                && k.close[i - 1] > k.close[i - 2]
1649                && k.open[i - 1] <= k.open[i - 2]
1650                && k.low[i - 1] < k.low[i - 2]
1651                && k.real_body(i) < k.average(&BODY_SHORT, i)
1652                && k.color(i) == 1
1653                && k.open[i] > k.low[i - 1]
1654            {
1655                100
1656            } else {
1657                0
1658            }
1659        },
1660    )
1661}
1662
1663/// Upside gap two crows: a long white candle, a short black gapping up, then a
1664/// black candle engulfing it but still closing above the first.
1665pub fn cdl_upsidegap2crows(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1666    let k = Candles::new(open, high, low, close);
1667    scan(
1668        k.len,
1669        BODY_SHORT.avg_period.max(BODY_LONG.avg_period) + 2,
1670        |i| {
1671            if k.color(i - 2) == 1
1672                && k.real_body(i - 2) > k.average(&BODY_LONG, i - 2)
1673                && k.color(i - 1) == -1
1674                && k.real_body(i - 1) <= k.average(&BODY_SHORT, i - 1)
1675                && k.real_body_gap_up(i - 1, i - 2)
1676                && k.color(i) == -1
1677                && k.open[i] > k.open[i - 1]
1678                && k.close[i] < k.close[i - 1]
1679                && k.close[i] > k.close[i - 2]
1680            {
1681                -100
1682            } else {
1683                0
1684            }
1685        },
1686    )
1687}
1688
1689/// Up/down-gap three methods: a gap between two same-colored candles filled by a
1690/// third opposite candle.
1691pub fn cdl_xsidegap3methods(open: &[f64], high: &[f64], low: &[f64], close: &[f64]) -> Vec<i32> {
1692    let k = Candles::new(open, high, low, close);
1693    scan(k.len, 2, |i| {
1694        if k.color(i - 2) == k.color(i - 1)
1695            && k.color(i - 1) == -k.color(i)
1696            && k.open[i] < k.body_top(i - 1)
1697            && k.open[i] > k.body_bottom(i - 1)
1698            && k.close[i] < k.body_top(i - 2)
1699            && k.close[i] > k.body_bottom(i - 2)
1700            && ((k.color(i - 2) == 1 && k.real_body_gap_up(i - 1, i - 2))
1701                || (k.color(i - 2) == -1 && k.real_body_gap_down(i - 1, i - 2)))
1702        {
1703            k.color(i - 2) * 100
1704        } else {
1705            0
1706        }
1707    })
1708}