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    fn update(&mut self, candle: Candle) -> Option<f64> {
69        self.has_emitted = true;
70        let prev = self.prev;
71        self.prev = Some(candle);
72        let Some(p) = prev else {
73            return Some(0.0);
74        };
75        let prev_red = p.close < p.open;
76        let prev_green = p.close > p.open;
77        let curr_green = candle.close > candle.open;
78        let curr_red = candle.close < candle.open;
79        let mid = f64::midpoint(p.open, p.close);
80        if prev_red
81            && curr_green
82            && candle.open < p.low
83            && candle.close > mid
84            && candle.close < p.open
85        {
86            Some(1.0)
87        } else if prev_green
88            && curr_red
89            && candle.open > p.high
90            && candle.close < mid
91            && candle.close > p.open
92        {
93            Some(-1.0)
94        } else {
95            Some(0.0)
96        }
97    }
98
99    fn reset(&mut self) {
100        self.prev = None;
101        self.has_emitted = false;
102    }
103
104    fn warmup_period(&self) -> usize {
105        2
106    }
107
108    fn is_ready(&self) -> bool {
109        self.has_emitted
110    }
111
112    fn name(&self) -> &'static str {
113        "PiercingDarkCloud"
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::traits::BatchExt;
121
122    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
123        Candle::new(open, high, low, close, 1.0, ts).unwrap()
124    }
125
126    #[test]
127    fn accessors_and_metadata() {
128        let p = PiercingDarkCloud::new();
129        assert_eq!(p.name(), "PiercingDarkCloud");
130        assert_eq!(p.warmup_period(), 2);
131        assert!(!p.is_ready());
132    }
133
134    #[test]
135    fn piercing_line_is_plus_one() {
136        let mut p = PiercingDarkCloud::new();
137        // Prev red: open 12, close 10. Curr green: opens at 9.8 (< prev low 10),
138        // closes at 11.5 (> midpoint 11, < prev open 12).
139        assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), Some(0.0));
140        assert_eq!(p.update(c(9.8, 11.8, 9.5, 11.5, 1)), Some(1.0));
141    }
142
143    #[test]
144    fn dark_cloud_cover_is_minus_one() {
145        let mut p = PiercingDarkCloud::new();
146        // Prev green: open 10, close 12. Curr red: opens 12.3 (> prev high 12.2),
147        // closes 10.5 (< midpoint 11, > prev open 10).
148        assert_eq!(p.update(c(10.0, 12.2, 9.5, 12.0, 0)), Some(0.0));
149        assert_eq!(p.update(c(12.3, 12.4, 10.4, 10.5, 1)), Some(-1.0));
150    }
151
152    #[test]
153    fn close_below_midpoint_is_not_piercing() {
154        let mut p = PiercingDarkCloud::new();
155        p.update(c(12.0, 12.5, 10.0, 10.0, 0));
156        // Closes only at 10.8 (below midpoint 11) -> not piercing.
157        assert_eq!(p.update(c(9.8, 11.0, 9.5, 10.8, 1)), Some(0.0));
158    }
159
160    #[test]
161    fn full_engulf_is_not_piercing() {
162        let mut p = PiercingDarkCloud::new();
163        p.update(c(12.0, 12.5, 10.0, 10.0, 0));
164        // Closes above prev.open (12) -> engulfs, not piercing.
165        assert_eq!(p.update(c(9.8, 13.0, 9.5, 12.5, 1)), Some(0.0));
166    }
167
168    #[test]
169    fn first_bar_returns_zero() {
170        let mut p = PiercingDarkCloud::new();
171        assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), Some(0.0));
172    }
173
174    #[test]
175    fn batch_equals_streaming() {
176        let candles: Vec<Candle> = (0..40)
177            .map(|i| {
178                let base = 100.0 + i as f64;
179                if i % 2 == 0 {
180                    c(base + 2.0, base + 2.5, base, base, i)
181                } else {
182                    c(base - 0.2, base + 1.8, base - 0.5, base + 1.5, i)
183                }
184            })
185            .collect();
186        let mut a = PiercingDarkCloud::new();
187        let mut b = PiercingDarkCloud::new();
188        assert_eq!(
189            a.batch(&candles),
190            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
191        );
192    }
193
194    #[test]
195    fn reset_clears_state() {
196        let mut p = PiercingDarkCloud::new();
197        p.update(c(12.0, 12.5, 10.0, 10.0, 0));
198        p.update(c(9.8, 11.8, 9.5, 11.5, 1));
199        assert!(p.is_ready());
200        p.reset();
201        assert!(!p.is_ready());
202        assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), Some(0.0));
203    }
204}