Skip to main content

wickra_core/indicators/
piercing_dark_cloud.rs

1//! Piercing Line / Dark Cloud Cover candlestick pattern.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Piercing Line / Dark Cloud Cover — a 2-bar reversal pattern.
7///
8/// **Piercing Line** (bullish, `+1.0`):
9/// ```text
10/// prev_red & curr_green
11///   & curr.open <  prev.low
12///   & curr.close > (prev.open + prev.close) / 2
13///   & curr.close <  prev.open
14/// ```
15///
16/// **Dark Cloud Cover** (bearish, `−1.0`):
17/// ```text
18/// prev_green & curr_red
19///   & curr.open >  prev.high
20///   & curr.close < (prev.open + prev.close) / 2
21///   & curr.close >  prev.open
22/// ```
23///
24/// Output is `+1.0` for a Piercing Line, `−1.0` for a Dark Cloud Cover, and
25/// `0.0` otherwise. The first bar always returns `0.0`. Pattern-shape check
26/// only — no trend filter is applied; combine with a trend indicator for
27/// actionable signals.
28///
29/// # Signed ±1 encoding
30///
31/// This detector already emits the uniform candlestick sign convention shared
32/// across the pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no
33/// pattern — so it drops straight into a machine-learning feature matrix where
34/// the bullish and bearish variants of the pattern occupy a single dimension.
35///
36/// # Example
37///
38/// ```
39/// use wickra_core::{Candle, Indicator, PiercingDarkCloud};
40///
41/// let mut indicator = PiercingDarkCloud::new();
42/// indicator.update(Candle::new(12.0, 12.5, 10.0, 10.0, 1.0, 0).unwrap());
43/// // Open below prev low, close above midpoint (11) but below prev open (12).
44/// let out = indicator
45///     .update(Candle::new(9.8, 11.8, 9.5, 11.5, 1.0, 1).unwrap());
46/// assert_eq!(out, Some(1.0));
47/// ```
48#[derive(Debug, Clone, Default)]
49pub struct PiercingDarkCloud {
50    prev: Option<Candle>,
51    has_emitted: bool,
52}
53
54impl PiercingDarkCloud {
55    /// Construct a new Piercing Line / Dark Cloud Cover detector.
56    pub const fn new() -> Self {
57        Self {
58            prev: None,
59            has_emitted: false,
60        }
61    }
62}
63
64impl Indicator for PiercingDarkCloud {
65    type Input = Candle;
66    type Output = f64;
67
68    #[inline]
69    fn update(&mut self, candle: Candle) -> Option<f64> {
70        let prev = self.prev;
71        self.prev = Some(candle);
72        let p = prev?;
73        self.has_emitted = true;
74        let prev_red = p.close < p.open;
75        let prev_green = p.close > p.open;
76        let curr_green = candle.close > candle.open;
77        let curr_red = candle.close < candle.open;
78        let mid = f64::midpoint(p.open, p.close);
79        if prev_red
80            && curr_green
81            && candle.open < p.low
82            && candle.close > mid
83            && candle.close < p.open
84        {
85            Some(1.0)
86        } else if prev_green
87            && curr_red
88            && candle.open > p.high
89            && candle.close < mid
90            && candle.close > p.open
91        {
92            Some(-1.0)
93        } else {
94            Some(0.0)
95        }
96    }
97
98    fn reset(&mut self) {
99        self.prev = None;
100        self.has_emitted = false;
101    }
102
103    #[inline]
104    fn warmup_period(&self) -> usize {
105        2
106    }
107
108    #[inline]
109    fn is_ready(&self) -> bool {
110        self.has_emitted
111    }
112
113    #[inline]
114    fn name(&self) -> &'static str {
115        "PiercingDarkCloud"
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::traits::BatchExt;
123
124    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
125        Candle::new(open, high, low, close, 1.0, ts).unwrap()
126    }
127
128    #[test]
129    fn accessors_and_metadata() {
130        let p = PiercingDarkCloud::new();
131        assert_eq!(p.name(), "PiercingDarkCloud");
132        assert_eq!(p.warmup_period(), 2);
133        assert!(!p.is_ready());
134    }
135
136    #[test]
137    fn piercing_line_is_plus_one() {
138        let mut p = PiercingDarkCloud::new();
139        // Prev red: open 12, close 10. Curr green: opens at 9.8 (< prev low 10),
140        // closes at 11.5 (> midpoint 11, < prev open 12).
141        assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), None);
142        assert_eq!(p.update(c(9.8, 11.8, 9.5, 11.5, 1)), Some(1.0));
143    }
144
145    #[test]
146    fn dark_cloud_cover_is_minus_one() {
147        let mut p = PiercingDarkCloud::new();
148        // Prev green: open 10, close 12. Curr red: opens 12.3 (> prev high 12.2),
149        // closes 10.5 (< midpoint 11, > prev open 10).
150        assert_eq!(p.update(c(10.0, 12.2, 9.5, 12.0, 0)), None);
151        assert_eq!(p.update(c(12.3, 12.4, 10.4, 10.5, 1)), Some(-1.0));
152    }
153
154    #[test]
155    fn close_below_midpoint_is_not_piercing() {
156        let mut p = PiercingDarkCloud::new();
157        p.update(c(12.0, 12.5, 10.0, 10.0, 0));
158        // Closes only at 10.8 (below midpoint 11) -> not piercing.
159        assert_eq!(p.update(c(9.8, 11.0, 9.5, 10.8, 1)), Some(0.0));
160    }
161
162    #[test]
163    fn full_engulf_is_not_piercing() {
164        let mut p = PiercingDarkCloud::new();
165        p.update(c(12.0, 12.5, 10.0, 10.0, 0));
166        // Closes above prev.open (12) -> engulfs, not piercing.
167        assert_eq!(p.update(c(9.8, 13.0, 9.5, 12.5, 1)), Some(0.0));
168    }
169
170    #[test]
171    fn first_bar_returns_zero() {
172        let mut p = PiercingDarkCloud::new();
173        assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), None);
174    }
175
176    #[test]
177    fn batch_equals_streaming() {
178        let candles: Vec<Candle> = (0..40)
179            .map(|i| {
180                let base = 100.0 + i as f64;
181                if i % 2 == 0 {
182                    c(base + 2.0, base + 2.5, base, base, i)
183                } else {
184                    c(base - 0.2, base + 1.8, base - 0.5, base + 1.5, i)
185                }
186            })
187            .collect();
188        let mut a = PiercingDarkCloud::new();
189        let mut b = PiercingDarkCloud::new();
190        assert_eq!(
191            a.batch(&candles),
192            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
193        );
194    }
195
196    #[test]
197    fn reset_clears_state() {
198        let mut p = PiercingDarkCloud::new();
199        p.update(c(12.0, 12.5, 10.0, 10.0, 0));
200        p.update(c(9.8, 11.8, 9.5, 11.5, 1));
201        assert!(p.is_ready());
202        p.reset();
203        assert!(!p.is_ready());
204        assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), None);
205    }
206}