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/// # Example
30///
31/// ```
32/// use wickra_core::{Candle, Indicator, PiercingDarkCloud};
33///
34/// let mut indicator = PiercingDarkCloud::new();
35/// indicator.update(Candle::new(12.0, 12.5, 10.0, 10.0, 1.0, 0).unwrap());
36/// // Open below prev low, close above midpoint (11) but below prev open (12).
37/// let out = indicator
38///     .update(Candle::new(9.8, 11.8, 9.5, 11.5, 1.0, 1).unwrap());
39/// assert_eq!(out, Some(1.0));
40/// ```
41#[derive(Debug, Clone, Default)]
42pub struct PiercingDarkCloud {
43    prev: Option<Candle>,
44    has_emitted: bool,
45}
46
47impl PiercingDarkCloud {
48    /// Construct a new Piercing Line / Dark Cloud Cover detector.
49    pub const fn new() -> Self {
50        Self {
51            prev: None,
52            has_emitted: false,
53        }
54    }
55}
56
57impl Indicator for PiercingDarkCloud {
58    type Input = Candle;
59    type Output = f64;
60
61    fn update(&mut self, candle: Candle) -> Option<f64> {
62        self.has_emitted = true;
63        let prev = self.prev;
64        self.prev = Some(candle);
65        let Some(p) = prev else {
66            return Some(0.0);
67        };
68        let prev_red = p.close < p.open;
69        let prev_green = p.close > p.open;
70        let curr_green = candle.close > candle.open;
71        let curr_red = candle.close < candle.open;
72        let mid = f64::midpoint(p.open, p.close);
73        if prev_red
74            && curr_green
75            && candle.open < p.low
76            && candle.close > mid
77            && candle.close < p.open
78        {
79            Some(1.0)
80        } else if prev_green
81            && curr_red
82            && candle.open > p.high
83            && candle.close < mid
84            && candle.close > p.open
85        {
86            Some(-1.0)
87        } else {
88            Some(0.0)
89        }
90    }
91
92    fn reset(&mut self) {
93        self.prev = None;
94        self.has_emitted = false;
95    }
96
97    fn warmup_period(&self) -> usize {
98        2
99    }
100
101    fn is_ready(&self) -> bool {
102        self.has_emitted
103    }
104
105    fn name(&self) -> &'static str {
106        "PiercingDarkCloud"
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use crate::traits::BatchExt;
114
115    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
116        Candle::new(open, high, low, close, 1.0, ts).unwrap()
117    }
118
119    #[test]
120    fn accessors_and_metadata() {
121        let p = PiercingDarkCloud::new();
122        assert_eq!(p.name(), "PiercingDarkCloud");
123        assert_eq!(p.warmup_period(), 2);
124        assert!(!p.is_ready());
125    }
126
127    #[test]
128    fn piercing_line_is_plus_one() {
129        let mut p = PiercingDarkCloud::new();
130        // Prev red: open 12, close 10. Curr green: opens at 9.8 (< prev low 10),
131        // closes at 11.5 (> midpoint 11, < prev open 12).
132        assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), Some(0.0));
133        assert_eq!(p.update(c(9.8, 11.8, 9.5, 11.5, 1)), Some(1.0));
134    }
135
136    #[test]
137    fn dark_cloud_cover_is_minus_one() {
138        let mut p = PiercingDarkCloud::new();
139        // Prev green: open 10, close 12. Curr red: opens 12.3 (> prev high 12.2),
140        // closes 10.5 (< midpoint 11, > prev open 10).
141        assert_eq!(p.update(c(10.0, 12.2, 9.5, 12.0, 0)), Some(0.0));
142        assert_eq!(p.update(c(12.3, 12.4, 10.4, 10.5, 1)), Some(-1.0));
143    }
144
145    #[test]
146    fn close_below_midpoint_is_not_piercing() {
147        let mut p = PiercingDarkCloud::new();
148        p.update(c(12.0, 12.5, 10.0, 10.0, 0));
149        // Closes only at 10.8 (below midpoint 11) -> not piercing.
150        assert_eq!(p.update(c(9.8, 11.0, 9.5, 10.8, 1)), Some(0.0));
151    }
152
153    #[test]
154    fn full_engulf_is_not_piercing() {
155        let mut p = PiercingDarkCloud::new();
156        p.update(c(12.0, 12.5, 10.0, 10.0, 0));
157        // Closes above prev.open (12) -> engulfs, not piercing.
158        assert_eq!(p.update(c(9.8, 13.0, 9.5, 12.5, 1)), Some(0.0));
159    }
160
161    #[test]
162    fn first_bar_returns_zero() {
163        let mut p = PiercingDarkCloud::new();
164        assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), Some(0.0));
165    }
166
167    #[test]
168    fn batch_equals_streaming() {
169        let candles: Vec<Candle> = (0..40)
170            .map(|i| {
171                let base = 100.0 + i as f64;
172                if i % 2 == 0 {
173                    c(base + 2.0, base + 2.5, base, base, i)
174                } else {
175                    c(base - 0.2, base + 1.8, base - 0.5, base + 1.5, i)
176                }
177            })
178            .collect();
179        let mut a = PiercingDarkCloud::new();
180        let mut b = PiercingDarkCloud::new();
181        assert_eq!(
182            a.batch(&candles),
183            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
184        );
185    }
186
187    #[test]
188    fn reset_clears_state() {
189        let mut p = PiercingDarkCloud::new();
190        p.update(c(12.0, 12.5, 10.0, 10.0, 0));
191        p.update(c(9.8, 11.8, 9.5, 11.5, 1));
192        assert!(p.is_ready());
193        p.reset();
194        assert!(!p.is_ready());
195        assert_eq!(p.update(c(12.0, 12.5, 10.0, 10.0, 0)), Some(0.0));
196    }
197}