Skip to main content

quantwave_core/indicators/patterns/
batch_d.rs

1//! Native O(1) streaming patterns for Batch D (three-bar patterns).
2
3use crate::indicators::patterns::candle_settings::*;
4use crate::traits::Next;
5
6/// CDL3BLACKCROWS
7#[derive(Debug, Clone)]
8#[allow(non_camel_case_types)]
9pub struct CDL3BLACKCROWS {
10    bars_seen: usize,
11    window: CandleWindow,
12    shadow_very_short_avg: RollingCandleAvg,
13}
14
15impl CDL3BLACKCROWS {
16    pub fn new() -> Self {
17        Self {
18            bars_seen: 0,
19            window: CandleWindow::new(4),
20            shadow_very_short_avg: RollingCandleAvg::new(SHADOW_VERY_SHORT),
21        }
22    }
23}
24
25impl Default for CDL3BLACKCROWS {
26    fn default() -> Self {
27        Self::new()
28    }
29}
30
31impl Next<(f64, f64, f64, f64)> for CDL3BLACKCROWS {
32    type Output = f64;
33
34    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
35        self.bars_seen += 1;
36        self.window.push(open, high, low, close);
37        self.shadow_very_short_avg.push(open, high, low, close);
38
39        if self.bars_seen <= SHADOW_VERY_SHORT.avg_period + 3 {
40            return 0.0;
41        }
42
43        if candle_color(self.window.bar(2).open, self.window.bar(2).close) == -1
44            && candle_color(self.window.bar(1).open, self.window.bar(1).close) == -1
45            && candle_color(self.window.bar(0).open, self.window.bar(0).close) == -1
46            && self.window.bar(1).close < self.window.bar(2).close
47            && self.window.bar(0).close < self.window.bar(1).close
48            && self.window.bar(2).open <= self.window.bar(3).open.max(self.window.bar(3).close)
49            && self.window.bar(1).open <= self.window.bar(2).open
50            && self.window.bar(1).open >= self.window.bar(2).close
51            && self.window.bar(0).open <= self.window.bar(1).open
52            && self.window.bar(0).open >= self.window.bar(1).close
53            && lower_shadow(
54                self.window.bar(2).open,
55                self.window.bar(2).low,
56                self.window.bar(2).close,
57            ) < self.shadow_very_short_avg.val(2)
58            && lower_shadow(
59                self.window.bar(1).open,
60                self.window.bar(1).low,
61                self.window.bar(1).close,
62            ) < self.shadow_very_short_avg.val(1)
63            && lower_shadow(
64                self.window.bar(0).open,
65                self.window.bar(0).low,
66                self.window.bar(0).close,
67            ) < self.shadow_very_short_avg.val(0)
68        {
69            return -100.0;
70        }
71        0.0
72    }
73}
74
75/// CDL3INSIDE
76#[derive(Debug, Clone)]
77#[allow(non_camel_case_types)]
78pub struct CDL3INSIDE {
79    bars_seen: usize,
80    window: CandleWindow,
81    body_long_avg: RollingCandleAvg,
82    body_short_avg: RollingCandleAvg,
83}
84
85impl CDL3INSIDE {
86    pub fn new() -> Self {
87        Self {
88            bars_seen: 0,
89            window: CandleWindow::new(3),
90            body_long_avg: RollingCandleAvg::new(BODY_LONG),
91            body_short_avg: RollingCandleAvg::new(BODY_SHORT),
92        }
93    }
94}
95
96impl Default for CDL3INSIDE {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102impl Next<(f64, f64, f64, f64)> for CDL3INSIDE {
103    type Output = f64;
104
105    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
106        self.bars_seen += 1;
107        self.window.push(open, high, low, close);
108        self.body_long_avg.push(open, high, low, close);
109        self.body_short_avg.push(open, high, low, close);
110
111        if self.bars_seen <= BODY_SHORT.avg_period.max(BODY_LONG.avg_period) + 2 {
112            return 0.0;
113        }
114
115        if real_body(self.window.bar(2).open, self.window.bar(2).close) > self.body_long_avg.val(2)
116            && real_body(self.window.bar(1).open, self.window.bar(1).close)
117                <= self.body_short_avg.val(1)
118            && self.window.bar(1).open.max(self.window.bar(1).close)
119                < self.window.bar(2).open.max(self.window.bar(2).close)
120            && self.window.bar(1).open.min(self.window.bar(1).close)
121                > self.window.bar(2).open.min(self.window.bar(2).close)
122        {
123            if candle_color(self.window.bar(2).open, self.window.bar(2).close) == 1
124                && candle_color(self.window.bar(0).open, self.window.bar(0).close) == -1
125                && self.window.bar(0).close < self.window.bar(2).open
126            {
127                return -100.0;
128            } else if candle_color(self.window.bar(2).open, self.window.bar(2).close) == -1
129                && candle_color(self.window.bar(0).open, self.window.bar(0).close) == 1
130                && self.window.bar(0).close > self.window.bar(2).open
131            {
132                return 100.0;
133            }
134        }
135        0.0
136    }
137}
138
139/// CDL3OUTSIDE
140#[derive(Debug, Clone)]
141#[allow(non_camel_case_types)]
142pub struct CDL3OUTSIDE {
143    bars_seen: usize,
144    window: CandleWindow,
145}
146
147impl CDL3OUTSIDE {
148    pub fn new() -> Self {
149        Self {
150            bars_seen: 0,
151            window: CandleWindow::new(3),
152        }
153    }
154}
155
156impl Default for CDL3OUTSIDE {
157    fn default() -> Self {
158        Self::new()
159    }
160}
161
162impl Next<(f64, f64, f64, f64)> for CDL3OUTSIDE {
163    type Output = f64;
164
165    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
166        self.bars_seen += 1;
167        self.window.push(open, high, low, close);
168
169        if self.bars_seen <= 2 {
170            return 0.0;
171        }
172
173        let bull = candle_color(self.window.bar(2).open, self.window.bar(2).close) == -1
174            && candle_color(self.window.bar(1).open, self.window.bar(1).close) == 1
175            && self.window.bar(1).close >= self.window.bar(2).open
176            && self.window.bar(1).open <= self.window.bar(2).close
177            && self.window.bar(0).close > self.window.bar(1).close;
178        let bear = candle_color(self.window.bar(2).open, self.window.bar(2).close) == 1
179            && candle_color(self.window.bar(1).open, self.window.bar(1).close) == -1
180            && self.window.bar(1).open >= self.window.bar(2).close
181            && self.window.bar(1).close <= self.window.bar(2).open
182            && self.window.bar(0).close < self.window.bar(1).close;
183        if bull {
184            return 100.0;
185        }
186        if bear {
187            return -100.0;
188        }
189        0.0
190    }
191}
192
193/// CDL3STARSINSOUTH
194#[derive(Debug, Clone)]
195#[allow(non_camel_case_types)]
196pub struct CDL3STARSINSOUTH {
197    bars_seen: usize,
198    window: CandleWindow,
199    body_long_avg: RollingCandleAvg,
200    shadow_long_avg: RollingCandleAvg,
201    body_short_avg: RollingCandleAvg,
202    shadow_vs_avg: RollingCandleAvg,
203}
204
205impl CDL3STARSINSOUTH {
206    pub fn new() -> Self {
207        Self {
208            bars_seen: 0,
209            window: CandleWindow::new(3),
210            body_long_avg: RollingCandleAvg::new(BODY_LONG),
211            shadow_long_avg: RollingCandleAvg::new(SHADOW_LONG),
212            body_short_avg: RollingCandleAvg::new(BODY_SHORT),
213            shadow_vs_avg: RollingCandleAvg::new(SHADOW_VERY_SHORT),
214        }
215    }
216}
217
218impl Default for CDL3STARSINSOUTH {
219    fn default() -> Self {
220        Self::new()
221    }
222}
223
224impl Next<(f64, f64, f64, f64)> for CDL3STARSINSOUTH {
225    type Output = f64;
226
227    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
228        self.bars_seen += 1;
229        self.window.push(open, high, low, close);
230        self.body_long_avg.push(open, high, low, close);
231        self.shadow_long_avg.push(open, high, low, close);
232        self.body_short_avg.push(open, high, low, close);
233        self.shadow_vs_avg.push(open, high, low, close);
234
235        // BODY_LONG, BODY_SHORT and SHADOW_VERY_SHORT all use avg_period 10;
236        // SHADOW_LONG.avg_period is 0 and cannot raise the max.
237        if self.bars_seen <= BODY_LONG.avg_period + 2 {
238            return 0.0;
239        }
240
241        if candle_color(self.window.bar(2).open, self.window.bar(2).close) == -1
242            && candle_color(self.window.bar(1).open, self.window.bar(1).close) == -1
243            && candle_color(self.window.bar(0).open, self.window.bar(0).close) == -1
244            && real_body(self.window.bar(2).open, self.window.bar(2).close)
245                > self.body_long_avg.val(2)
246            && lower_shadow(
247                self.window.bar(2).open,
248                self.window.bar(2).low,
249                self.window.bar(2).close,
250            ) > self.shadow_long_avg.val(2)
251            && self.window.bar(1).open.min(self.window.bar(1).close)
252                > self.window.bar(2).open.min(self.window.bar(2).close)
253            && self.window.bar(1).open.max(self.window.bar(1).close)
254                < self.window.bar(2).open.max(self.window.bar(2).close)
255            && self.window.bar(1).low < self.window.bar(2).low
256            // 3rd: short body, both shadows short, range inside the 2nd's range
257            && real_body(self.window.bar(0).open, self.window.bar(0).close)
258                < self.body_short_avg.val(0)
259            && upper_shadow(
260                self.window.bar(0).open,
261                self.window.bar(0).high,
262                self.window.bar(0).close,
263            ) < self.shadow_vs_avg.val(0)
264            && lower_shadow(
265                self.window.bar(0).open,
266                self.window.bar(0).low,
267                self.window.bar(0).close,
268            ) < self.shadow_vs_avg.val(0)
269            && self.window.bar(0).low > self.window.bar(1).low
270            && self.window.bar(0).high < self.window.bar(1).high
271        {
272            return 100.0;
273        }
274        0.0
275    }
276}
277
278/// CDL3WHITESOLDIERS
279#[derive(Debug, Clone)]
280#[allow(non_camel_case_types)]
281pub struct CDL3WHITESOLDIERS {
282    bars_seen: usize,
283    window: CandleWindow,
284    near_avg: RollingCandleAvg,
285    far_avg: RollingCandleAvg,
286    body_short_avg: RollingCandleAvg,
287    shadow_very_short_avg: RollingCandleAvg,
288}
289
290impl CDL3WHITESOLDIERS {
291    pub fn new() -> Self {
292        Self {
293            bars_seen: 0,
294            window: CandleWindow::new(4),
295            near_avg: RollingCandleAvg::new(NEAR),
296            far_avg: RollingCandleAvg::new(FAR),
297            body_short_avg: RollingCandleAvg::new(BODY_SHORT),
298            shadow_very_short_avg: RollingCandleAvg::new(SHADOW_VERY_SHORT),
299        }
300    }
301}
302
303impl Default for CDL3WHITESOLDIERS {
304    fn default() -> Self {
305        Self::new()
306    }
307}
308
309impl Next<(f64, f64, f64, f64)> for CDL3WHITESOLDIERS {
310    type Output = f64;
311
312    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
313        self.bars_seen += 1;
314        self.window.push(open, high, low, close);
315        self.near_avg.push(open, high, low, close);
316        self.far_avg.push(open, high, low, close);
317        self.body_short_avg.push(open, high, low, close);
318        self.shadow_very_short_avg.push(open, high, low, close);
319
320        // Oracle lookback: max(SVS, BODY_SHORT, FAR, NEAR) + 2
321        let lookback = SHADOW_VERY_SHORT
322            .avg_period
323            .max(BODY_SHORT.avg_period)
324            .max(FAR.avg_period)
325            .max(NEAR.avg_period)
326            + 2;
327        if self.bars_seen <= lookback {
328            return 0.0;
329        }
330
331        let b2 = self.window.bar(2);
332        let b1 = self.window.bar(1);
333        let b0 = self.window.bar(0);
334
335        if candle_color(b2.open, b2.close) == 1
336            && candle_color(b1.open, b1.close) == 1
337            && candle_color(b0.open, b0.close) == 1
338            && b1.close > b2.close
339            && b0.close > b1.close
340            // Short upper shadows
341            && upper_shadow(b2.open, b2.high, b2.close) < self.shadow_very_short_avg.val(2)
342            && upper_shadow(b1.open, b1.high, b1.close) < self.shadow_very_short_avg.val(1)
343            && upper_shadow(b0.open, b0.high, b0.close) < self.shadow_very_short_avg.val(0)
344            // Opens within or near the previous body
345            && b1.open > b2.open
346            && b1.open <= b2.close + self.near_avg.val(1)
347            && b0.open > b1.open
348            && b0.open <= b1.close + self.near_avg.val(0)
349            // Bodies not far shorter than the prior one
350            && real_body(b1.open, b1.close) > real_body(b2.open, b2.close) - self.far_avg.val(1)
351            && real_body(b0.open, b0.close) > real_body(b1.open, b1.close) - self.far_avg.val(0)
352            // Last body not short
353            && real_body(b0.open, b0.close) > self.body_short_avg.val(0)
354        {
355            return 100.0;
356        }
357        0.0
358    }
359}
360
361/// CDLABANDONEDBABY
362#[derive(Debug, Clone)]
363#[allow(non_camel_case_types)]
364pub struct CDLABANDONEDBABY {
365    bars_seen: usize,
366    window: CandleWindow,
367    body_doji_avg: RollingCandleAvg,
368    body_long_avg: RollingCandleAvg,
369    body_short_avg: RollingCandleAvg,
370}
371
372impl CDLABANDONEDBABY {
373    pub fn new() -> Self {
374        Self {
375            bars_seen: 0,
376            window: CandleWindow::new(3),
377            body_doji_avg: RollingCandleAvg::new(BODY_DOJI),
378            body_long_avg: RollingCandleAvg::new(BODY_LONG),
379            body_short_avg: RollingCandleAvg::new(BODY_SHORT),
380        }
381    }
382}
383
384impl Default for CDLABANDONEDBABY {
385    fn default() -> Self {
386        Self::new()
387    }
388}
389
390impl Next<(f64, f64, f64, f64)> for CDLABANDONEDBABY {
391    type Output = f64;
392
393    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
394        self.bars_seen += 1;
395        self.window.push(open, high, low, close);
396        self.body_doji_avg.push(open, high, low, close);
397        self.body_long_avg.push(open, high, low, close);
398        self.body_short_avg.push(open, high, low, close);
399
400        if self.bars_seen
401            <= BODY_DOJI
402                .avg_period
403                .max(BODY_LONG.avg_period)
404                .max(BODY_SHORT.avg_period)
405                + 2
406        {
407            return 0.0;
408        }
409
410        let penetration = 0.3;
411        let base = real_body(self.window.bar(2).open, self.window.bar(2).close)
412            > self.body_long_avg.val(2)
413            && real_body(self.window.bar(1).open, self.window.bar(1).close)
414                <= self.body_doji_avg.val(1)
415            && real_body(self.window.bar(0).open, self.window.bar(0).close)
416                > self.body_short_avg.val(0);
417        let bull = base
418            && candle_color(self.window.bar(2).open, self.window.bar(2).close) == -1
419            && candle_color(self.window.bar(0).open, self.window.bar(0).close) == 1
420            && self.window.bar(1).high < self.window.bar(2).low
421            && self.window.bar(0).low > self.window.bar(1).high
422            && self.window.bar(0).close
423                > self.window.bar(2).close
424                    + real_body(self.window.bar(2).open, self.window.bar(2).close) * penetration;
425        let bear = base
426            && candle_color(self.window.bar(2).open, self.window.bar(2).close) == 1
427            && candle_color(self.window.bar(0).open, self.window.bar(0).close) == -1
428            && self.window.bar(1).low > self.window.bar(2).high
429            && self.window.bar(0).high < self.window.bar(1).low
430            && self.window.bar(0).close
431                < self.window.bar(2).close
432                    - real_body(self.window.bar(2).open, self.window.bar(2).close) * penetration;
433        if bull {
434            return 100.0;
435        }
436        if bear {
437            return -100.0;
438        }
439        0.0
440    }
441}
442
443/// CDLADVANCEBLOCK
444#[derive(Debug, Clone)]
445#[allow(non_camel_case_types)]
446pub struct CDLADVANCEBLOCK {
447    bars_seen: usize,
448    window: CandleWindow,
449    far_avg: RollingCandleAvg,
450    near_avg: RollingCandleAvg,
451    shadow_long_avg: RollingCandleAvg,
452    shadow_short_avg: RollingCandleAvg,
453    body_long_avg: RollingCandleAvg,
454}
455
456impl CDLADVANCEBLOCK {
457    pub fn new() -> Self {
458        Self {
459            bars_seen: 0,
460            window: CandleWindow::new(3),
461            far_avg: RollingCandleAvg::new(FAR),
462            near_avg: RollingCandleAvg::new(NEAR),
463            shadow_long_avg: RollingCandleAvg::new(SHADOW_LONG),
464            shadow_short_avg: RollingCandleAvg::new(SHADOW_SHORT),
465            body_long_avg: RollingCandleAvg::new(BODY_LONG),
466        }
467    }
468}
469
470impl Default for CDLADVANCEBLOCK {
471    fn default() -> Self {
472        Self::new()
473    }
474}
475
476impl Next<(f64, f64, f64, f64)> for CDLADVANCEBLOCK {
477    type Output = f64;
478
479    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
480        self.bars_seen += 1;
481        self.window.push(open, high, low, close);
482        self.far_avg.push(open, high, low, close);
483        self.near_avg.push(open, high, low, close);
484        self.shadow_long_avg.push(open, high, low, close);
485        self.shadow_short_avg.push(open, high, low, close);
486        self.body_long_avg.push(open, high, low, close);
487
488        // Oracle lookback: max(SHADOW_LONG, SHADOW_SHORT, FAR, NEAR, BODY_LONG) + 2
489        // SHADOW_LONG.avg_period is 0, so it cannot raise the max.
490        let lookback = SHADOW_SHORT
491            .avg_period
492            .max(FAR.avg_period)
493            .max(NEAR.avg_period)
494            .max(BODY_LONG.avg_period)
495            + 2;
496        if self.bars_seen <= lookback {
497            return 0.0;
498        }
499
500        let b2 = self.window.bar(2);
501        let b1 = self.window.bar(1);
502        let b0 = self.window.bar(0);
503
504        let rb2 = real_body(b2.open, b2.close);
505        let rb1 = real_body(b1.open, b1.close);
506        let rb0 = real_body(b0.open, b0.close);
507
508        // Three white candles with rising closes, opens within/near the prior body,
509        // first candle long-bodied with a short upper shadow.
510        let base = candle_color(b2.open, b2.close) == 1
511            && candle_color(b1.open, b1.close) == 1
512            && candle_color(b0.open, b0.close) == 1
513            && b1.close > b2.close
514            && b0.close > b1.close
515            && b1.open > b2.open
516            && b1.open <= b2.close + self.near_avg.val(1)
517            && b0.open > b1.open
518            && b0.open <= b1.close + self.near_avg.val(0)
519            && rb2 > self.body_long_avg.val(2)
520            && upper_shadow(b2.open, b2.high, b2.close) < self.shadow_short_avg.val(2);
521
522        // Weakness: bodies shrinking and/or upper shadows lengthening.
523        let weakness = base
524            && ((rb1 < rb2 - self.far_avg.val(1) && rb0 < rb1 + self.near_avg.val(0))
525                || (rb0 < rb1
526                    && rb1 < rb2
527                    && (upper_shadow(b0.open, b0.high, b0.close) > self.shadow_long_avg.val(0)
528                        || upper_shadow(b1.open, b1.high, b1.close)
529                            > self.shadow_long_avg.val(1)))
530                || (rb0 < rb1 - self.far_avg.val(0)));
531
532        if weakness { -100.0 } else { 0.0 }
533    }
534}
535
536/// CDLEVENINGDOJISTAR
537#[derive(Debug, Clone)]
538#[allow(non_camel_case_types)]
539pub struct CDLEVENINGDOJISTAR {
540    bars_seen: usize,
541    window: CandleWindow,
542    body_doji_avg: RollingCandleAvg,
543    body_long_avg: RollingCandleAvg,
544    body_short_avg: RollingCandleAvg,
545}
546
547impl CDLEVENINGDOJISTAR {
548    pub fn new() -> Self {
549        Self {
550            bars_seen: 0,
551            window: CandleWindow::new(3),
552            body_doji_avg: RollingCandleAvg::new(BODY_DOJI),
553            body_long_avg: RollingCandleAvg::new(BODY_LONG),
554            body_short_avg: RollingCandleAvg::new(BODY_SHORT),
555        }
556    }
557}
558
559impl Default for CDLEVENINGDOJISTAR {
560    fn default() -> Self {
561        Self::new()
562    }
563}
564
565impl Next<(f64, f64, f64, f64)> for CDLEVENINGDOJISTAR {
566    type Output = f64;
567
568    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
569        self.bars_seen += 1;
570        self.window.push(open, high, low, close);
571        self.body_doji_avg.push(open, high, low, close);
572        self.body_long_avg.push(open, high, low, close);
573        self.body_short_avg.push(open, high, low, close);
574
575        if self.bars_seen
576            <= BODY_DOJI
577                .avg_period
578                .max(BODY_LONG.avg_period)
579                .max(BODY_SHORT.avg_period)
580                + 2
581        {
582            return 0.0;
583        }
584
585        let penetration = 0.3;
586        if candle_color(self.window.bar(2).open, self.window.bar(2).close) == 1
587            && real_body(self.window.bar(2).open, self.window.bar(2).close)
588                > self.body_long_avg.val(2)
589            && real_body(self.window.bar(1).open, self.window.bar(1).close)
590                <= self.body_doji_avg.val(1)
591            && (self.window.bar(1).open.min(self.window.bar(1).close)
592                > self.window.bar(2).open.max(self.window.bar(2).close))
593            && candle_color(self.window.bar(0).open, self.window.bar(0).close) == -1
594            && real_body(self.window.bar(0).open, self.window.bar(0).close)
595                > self.body_short_avg.val(0)
596            && self.window.bar(0).close
597                < self.window.bar(2).close
598                    - real_body(self.window.bar(2).open, self.window.bar(2).close) * penetration
599        {
600            return -100.0;
601        }
602        0.0
603    }
604}
605
606/// CDLEVENINGSTAR
607#[derive(Debug, Clone)]
608#[allow(non_camel_case_types)]
609pub struct CDLEVENINGSTAR {
610    bars_seen: usize,
611    window: CandleWindow,
612    body_long_avg: RollingCandleAvg,
613    body_short_avg: RollingCandleAvg,
614}
615
616impl CDLEVENINGSTAR {
617    pub fn new() -> Self {
618        Self {
619            bars_seen: 0,
620            window: CandleWindow::new(3),
621            body_long_avg: RollingCandleAvg::new(BODY_LONG),
622            body_short_avg: RollingCandleAvg::new(BODY_SHORT),
623        }
624    }
625}
626
627impl Default for CDLEVENINGSTAR {
628    fn default() -> Self {
629        Self::new()
630    }
631}
632
633impl Next<(f64, f64, f64, f64)> for CDLEVENINGSTAR {
634    type Output = f64;
635
636    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
637        self.bars_seen += 1;
638        self.window.push(open, high, low, close);
639        self.body_long_avg.push(open, high, low, close);
640        self.body_short_avg.push(open, high, low, close);
641
642        if self.bars_seen <= BODY_LONG.avg_period.max(BODY_SHORT.avg_period) + 2 {
643            return 0.0;
644        }
645
646        let penetration = 0.3;
647        if candle_color(self.window.bar(2).open, self.window.bar(2).close) == 1
648            && real_body(self.window.bar(2).open, self.window.bar(2).close)
649                > self.body_long_avg.val(2)
650            && real_body(self.window.bar(1).open, self.window.bar(1).close)
651                <= self.body_short_avg.val(1)
652            && (self.window.bar(1).open.min(self.window.bar(1).close)
653                > self.window.bar(2).open.max(self.window.bar(2).close))
654            && candle_color(self.window.bar(0).open, self.window.bar(0).close) == -1
655            && real_body(self.window.bar(0).open, self.window.bar(0).close)
656                > self.body_short_avg.val(0)
657            && self.window.bar(0).close
658                < self.window.bar(2).close
659                    - real_body(self.window.bar(2).open, self.window.bar(2).close) * penetration
660        {
661            return -100.0;
662        }
663        0.0
664    }
665}
666
667/// CDLMORNINGDOJISTAR
668#[derive(Debug, Clone)]
669#[allow(non_camel_case_types)]
670pub struct CDLMORNINGDOJISTAR {
671    bars_seen: usize,
672    window: CandleWindow,
673    body_doji_avg: RollingCandleAvg,
674    body_long_avg: RollingCandleAvg,
675    body_short_avg: RollingCandleAvg,
676}
677
678impl CDLMORNINGDOJISTAR {
679    pub fn new() -> Self {
680        Self {
681            bars_seen: 0,
682            window: CandleWindow::new(3),
683            body_doji_avg: RollingCandleAvg::new(BODY_DOJI),
684            body_long_avg: RollingCandleAvg::new(BODY_LONG),
685            body_short_avg: RollingCandleAvg::new(BODY_SHORT),
686        }
687    }
688}
689
690impl Default for CDLMORNINGDOJISTAR {
691    fn default() -> Self {
692        Self::new()
693    }
694}
695
696impl Next<(f64, f64, f64, f64)> for CDLMORNINGDOJISTAR {
697    type Output = f64;
698
699    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
700        self.bars_seen += 1;
701        self.window.push(open, high, low, close);
702        self.body_doji_avg.push(open, high, low, close);
703        self.body_long_avg.push(open, high, low, close);
704        self.body_short_avg.push(open, high, low, close);
705
706        if self.bars_seen
707            <= BODY_DOJI
708                .avg_period
709                .max(BODY_LONG.avg_period)
710                .max(BODY_SHORT.avg_period)
711                + 2
712        {
713            return 0.0;
714        }
715
716        let penetration = 0.3;
717        if candle_color(self.window.bar(2).open, self.window.bar(2).close) == -1
718            && real_body(self.window.bar(2).open, self.window.bar(2).close)
719                > self.body_long_avg.val(2)
720            && real_body(self.window.bar(1).open, self.window.bar(1).close)
721                <= self.body_doji_avg.val(1)
722            && (self.window.bar(1).open.max(self.window.bar(1).close)
723                < self.window.bar(2).open.min(self.window.bar(2).close))
724            && candle_color(self.window.bar(0).open, self.window.bar(0).close) == 1
725            && real_body(self.window.bar(0).open, self.window.bar(0).close)
726                > self.body_short_avg.val(0)
727            && self.window.bar(0).close
728                > self.window.bar(2).close
729                    + real_body(self.window.bar(2).open, self.window.bar(2).close) * penetration
730        {
731            return 100.0;
732        }
733        0.0
734    }
735}
736
737/// CDLMORNINGSTAR
738#[derive(Debug, Clone)]
739#[allow(non_camel_case_types)]
740pub struct CDLMORNINGSTAR {
741    bars_seen: usize,
742    window: CandleWindow,
743    body_long_avg: RollingCandleAvg,
744    body_short_avg: RollingCandleAvg,
745}
746
747impl CDLMORNINGSTAR {
748    pub fn new() -> Self {
749        Self {
750            bars_seen: 0,
751            window: CandleWindow::new(3),
752            body_long_avg: RollingCandleAvg::new(BODY_LONG),
753            body_short_avg: RollingCandleAvg::new(BODY_SHORT),
754        }
755    }
756}
757
758impl Default for CDLMORNINGSTAR {
759    fn default() -> Self {
760        Self::new()
761    }
762}
763
764impl Next<(f64, f64, f64, f64)> for CDLMORNINGSTAR {
765    type Output = f64;
766
767    fn next(&mut self, (open, high, low, close): (f64, f64, f64, f64)) -> Self::Output {
768        self.bars_seen += 1;
769        self.window.push(open, high, low, close);
770        self.body_long_avg.push(open, high, low, close);
771        self.body_short_avg.push(open, high, low, close);
772
773        if self.bars_seen <= BODY_LONG.avg_period.max(BODY_SHORT.avg_period) + 2 {
774            return 0.0;
775        }
776
777        let penetration = 0.3;
778        if candle_color(self.window.bar(2).open, self.window.bar(2).close) == -1
779            && real_body(self.window.bar(2).open, self.window.bar(2).close)
780                > self.body_long_avg.val(2)
781            && real_body(self.window.bar(1).open, self.window.bar(1).close)
782                <= self.body_short_avg.val(1)
783            && (self.window.bar(1).open.max(self.window.bar(1).close)
784                < self.window.bar(2).open.min(self.window.bar(2).close))
785            && candle_color(self.window.bar(0).open, self.window.bar(0).close) == 1
786            && real_body(self.window.bar(0).open, self.window.bar(0).close)
787                > self.body_short_avg.val(0)
788            && self.window.bar(0).close
789                > self.window.bar(2).close
790                    + real_body(self.window.bar(2).open, self.window.bar(2).close) * penetration
791        {
792            return 100.0;
793        }
794        0.0
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use crate::indicators::patterns::fixtures as fx;
802    // talib-rs's cdl_3blackcrows is defective; the native implementation is correct
803    // and is deliberately NOT made bug-compatible. Evidence:
804    //
805    //  * talib-rs seeds `shadow_sum[k]` at `bar_offset = start - 3 + k` but consumes
806    //    that slot as the average for bar `i - 2 + k` — off by one — and then
807    //    adds/subtracts against the skewed set, so the window becomes non-contiguous
808    //    and corresponds to no principled rolling average.
809    //  * Over 200,000 bars: a from-scratch model of C TA-Lib semantics gives 8
810    //    mismatches against the oracle; a one-bar-shifted model gives 11; the native
811    //    implementation gives exactly the same 8 as the C-correct model. So
812    //    native == C TA-Lib and the oracle is the outlier.
813    //
814    // Measured divergence: 8 bars / 200,000 on the deterministic walk, and never
815    // more than 1 per run over 40,000 random walks of length <= 200. Bound of 12
816    // leaves headroom while staying well below the oracle's 33 non-zero signals.
817    crate::test_pattern_parity!(
818        test_cdl3blackcrows_parity,
819        CDL3BLACKCROWS,
820        talib_rs::pattern::cdl_3blackcrows,
821        fx::three_black_crows,
822        oracle_exempt = "talib-rs's cdl_3blackcrows seeds shadow_sum[k] at bar `start-3+k` but \
823                         uses it for bar `i-2+k`, then rolls that skewed set, producing a \
824                         non-contiguous window matching no principled rolling average. A \
825                         from-scratch C-TA-Lib-semantics model and the native implementation \
826                         produce the identical 8 mismatches vs the oracle over 200,000 bars; a \
827                         shifted model produces 11. Native == C TA-Lib; the oracle is wrong.",
828        max_mismatches = 12,
829        reference_mismatches = 8
830    );
831    crate::test_pattern_parity!(
832        test_cdl3inside_parity,
833        CDL3INSIDE,
834        talib_rs::pattern::cdl_3inside,
835        fx::three_inside
836    );
837    crate::test_pattern_parity!(
838        test_cdl3outside_parity,
839        CDL3OUTSIDE,
840        talib_rs::pattern::cdl_3outside
841    );
842    crate::test_pattern_parity!(
843        test_cdl3starsinsouth_parity,
844        CDL3STARSINSOUTH,
845        talib_rs::pattern::cdl_3starsinsouth,
846        fx::three_stars_in_south
847    );
848    crate::test_pattern_parity!(
849        test_cdl3whitesoldiers_parity,
850        CDL3WHITESOLDIERS,
851        talib_rs::pattern::cdl_3whitesoldiers
852    );
853    crate::test_pattern_parity!(
854        test_cdlabandonedbaby_parity,
855        CDLABANDONEDBABY,
856        talib_rs::pattern::cdl_abandonedbaby,
857        fx::abandonedbaby
858    );
859    crate::test_pattern_parity!(
860        test_cdladvanceblock_parity,
861        CDLADVANCEBLOCK,
862        talib_rs::pattern::cdl_advanceblock
863    );
864    crate::test_pattern_parity!(
865        test_cdleveningdojistar_parity,
866        CDLEVENINGDOJISTAR,
867        talib_rs::pattern::cdl_eveningdojistar,
868        fx::eveningdojistar
869    );
870    crate::test_pattern_parity!(
871        test_cdleveningstar_parity,
872        CDLEVENINGSTAR,
873        talib_rs::pattern::cdl_eveningstar,
874        fx::eveningstar
875    );
876    crate::test_pattern_parity!(
877        test_cdlmorningdojistar_parity,
878        CDLMORNINGDOJISTAR,
879        talib_rs::pattern::cdl_morningdojistar,
880        fx::morningdojistar
881    );
882    crate::test_pattern_parity!(
883        test_cdlmorningstar_parity,
884        CDLMORNINGSTAR,
885        talib_rs::pattern::cdl_morningstar,
886        fx::morningstar
887    );
888}