Skip to main content

wickra_core/indicators/
concealing_baby_swallow.rs

1//! Concealing Baby Swallow candlestick pattern.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Returns `true` when `candle` is a black marubozu: a down candle whose body fills
7/// the range with negligible shadows on both ends.
8fn black_marubozu(candle: Candle) -> bool {
9    let range = candle.high - candle.low;
10    if range <= 0.0 {
11        return false;
12    }
13    let upper = candle.high - candle.open;
14    let lower = candle.close - candle.low;
15    candle.open > candle.close && upper <= 0.05 * range && lower <= 0.05 * range
16}
17
18/// Concealing Baby Swallow — a rare 4-bar bullish reversal. Two black marubozu lead
19/// a steep decline; the third is a black candle that gaps down on the open yet
20/// throws a long upper shadow back up into the second body; the fourth is a large
21/// black candle that completely engulfs the third, shadows included. The relentless
22/// selling that can no longer make ground signals capitulation.
23///
24/// ```text
25/// bar1, bar2 black marubozu (body == range, negligible shadows)
26/// bar3 black, opens below bar2's body (open3 < close2) with an upper
27///   shadow into it (high3 > close2)
28/// bar4 black, engulfs bar3 including shadows: open4 > high3 and close4 < low3
29/// ```
30///
31/// Output is `+1.0` when the pattern completes and `0.0` otherwise. Concealing Baby
32/// Swallow is a single-direction (bullish-only) reversal, so it never emits `−1.0`.
33/// The first three bars always return `0.0` because the four-bar window is not yet
34/// filled. Body and shadow thresholds follow the geometric house style rather than
35/// TA-Lib's rolling averages. Pattern-shape check only — no trend filter is applied;
36/// combine with a trend indicator for actionable signals.
37///
38/// # Signed ±1 encoding
39///
40/// This detector emits the uniform candlestick sign convention shared across the
41/// pattern family — `+1.0` bullish, `0.0` no pattern — so it drops straight into
42/// a machine-learning feature matrix as a single dimension.
43///
44/// # Example
45///
46/// ```
47/// use wickra_core::{Candle, ConcealingBabySwallow, Indicator};
48///
49/// let mut indicator = ConcealingBabySwallow::new();
50/// indicator.update(Candle::new(20.0, 20.1, 14.9, 15.0, 1.0, 0).unwrap());
51/// indicator.update(Candle::new(16.0, 16.1, 11.9, 12.0, 1.0, 1).unwrap());
52/// indicator.update(Candle::new(11.0, 13.0, 9.9, 10.0, 1.0, 2).unwrap());
53/// let out = indicator
54///     .update(Candle::new(14.0, 14.1, 8.9, 9.0, 1.0, 3).unwrap());
55/// assert_eq!(out, Some(1.0));
56/// ```
57#[derive(Debug, Clone, Default)]
58pub struct ConcealingBabySwallow {
59    c1: Option<Candle>,
60    c2: Option<Candle>,
61    c3: Option<Candle>,
62    has_emitted: bool,
63}
64
65impl ConcealingBabySwallow {
66    /// Construct a new Concealing Baby Swallow detector.
67    pub const fn new() -> Self {
68        Self {
69            c1: None,
70            c2: None,
71            c3: None,
72            has_emitted: false,
73        }
74    }
75}
76
77impl Indicator for ConcealingBabySwallow {
78    type Input = Candle;
79    type Output = f64;
80
81    #[inline]
82    fn update(&mut self, candle: Candle) -> Option<f64> {
83        let bar1 = self.c1;
84        let bar2 = self.c2;
85        let bar3 = self.c3;
86        self.c1 = self.c2;
87        self.c2 = self.c3;
88        self.c3 = Some(candle);
89        let (Some(bar1), Some(bar2), Some(bar3)) = (bar1, bar2, bar3) else {
90            return None;
91        };
92        self.has_emitted = true;
93        // bar1 and bar2 are black marubozu.
94        if !black_marubozu(bar1) || !black_marubozu(bar2) {
95            return Some(0.0);
96        }
97        // bar3 is black, gaps down on the open, throws an upper shadow into bar2.
98        if bar3.open <= bar3.close {
99            return Some(0.0);
100        }
101        if bar3.open >= bar2.close {
102            return Some(0.0); // no downside open gap
103        }
104        if bar3.high <= bar2.close {
105            return Some(0.0); // upper shadow does not reach into bar2's body
106        }
107        // bar4 is black and engulfs bar3 including its shadows.
108        if candle.open <= candle.close {
109            return Some(0.0);
110        }
111        if candle.open > bar3.high && candle.close < bar3.low {
112            return Some(1.0);
113        }
114        Some(0.0)
115    }
116
117    fn reset(&mut self) {
118        self.c1 = None;
119        self.c2 = None;
120        self.c3 = None;
121        self.has_emitted = false;
122    }
123
124    #[inline]
125    fn warmup_period(&self) -> usize {
126        4
127    }
128
129    #[inline]
130    fn is_ready(&self) -> bool {
131        self.has_emitted
132    }
133
134    #[inline]
135    fn name(&self) -> &'static str {
136        "ConcealingBabySwallow"
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::traits::BatchExt;
144
145    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
146        Candle::new(open, high, low, close, 1.0, ts).unwrap()
147    }
148
149    #[test]
150    fn accessors_and_metadata() {
151        let t = ConcealingBabySwallow::new();
152        assert_eq!(t.name(), "ConcealingBabySwallow");
153        assert_eq!(t.warmup_period(), 4);
154        assert!(!t.is_ready());
155    }
156
157    #[test]
158    fn concealing_baby_swallow_is_plus_one() {
159        let mut t = ConcealingBabySwallow::new();
160        assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), None);
161        assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 1)), None);
162        assert_eq!(t.update(c(11.0, 13.0, 9.9, 10.0, 2)), None);
163        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(1.0));
164    }
165
166    #[test]
167    fn warmup_returns_zero() {
168        let mut t = ConcealingBabySwallow::new();
169        assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), None);
170        assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 1)), None);
171        assert_eq!(t.update(c(11.0, 13.0, 9.9, 10.0, 2)), None);
172    }
173
174    #[test]
175    fn first_bar_not_marubozu_yields_zero() {
176        let mut t = ConcealingBabySwallow::new();
177        // bar1 white.
178        t.update(c(15.0, 20.1, 14.9, 20.0, 0));
179        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
180        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
181        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
182    }
183
184    #[test]
185    fn first_bar_zero_range_yields_zero() {
186        let mut t = ConcealingBabySwallow::new();
187        // bar1 zero range -> not a marubozu.
188        t.update(c(15.0, 15.0, 15.0, 15.0, 0));
189        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
190        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
191        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
192    }
193
194    #[test]
195    fn second_bar_not_marubozu_yields_zero() {
196        let mut t = ConcealingBabySwallow::new();
197        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
198        // bar2 white.
199        t.update(c(12.0, 16.1, 11.9, 16.0, 1));
200        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
201        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
202    }
203
204    #[test]
205    fn third_bar_not_black_yields_zero() {
206        let mut t = ConcealingBabySwallow::new();
207        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
208        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
209        // bar3 white.
210        t.update(c(11.0, 13.0, 9.9, 12.5, 2));
211        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
212    }
213
214    #[test]
215    fn third_bar_no_gap_yields_zero() {
216        let mut t = ConcealingBabySwallow::new();
217        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
218        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
219        // bar3 black but opens at/above bar2's close -> no downside gap.
220        t.update(c(12.5, 13.0, 9.9, 10.0, 2));
221        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
222    }
223
224    #[test]
225    fn third_bar_no_upper_shadow_yields_zero() {
226        let mut t = ConcealingBabySwallow::new();
227        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
228        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
229        // bar3 black, gaps down, but its high does not reach into bar2's body.
230        t.update(c(11.0, 11.5, 9.9, 10.0, 2));
231        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
232    }
233
234    #[test]
235    fn fourth_bar_not_black_yields_zero() {
236        let mut t = ConcealingBabySwallow::new();
237        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
238        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
239        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
240        // bar4 white.
241        assert_eq!(t.update(c(14.0, 14.1, 8.9, 14.05, 3)), Some(0.0));
242    }
243
244    #[test]
245    fn fourth_bar_not_engulfing_yields_zero() {
246        let mut t = ConcealingBabySwallow::new();
247        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
248        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
249        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
250        // bar4 black but does not engulf bar3's high.
251        assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 3)), Some(0.0));
252    }
253
254    #[test]
255    fn batch_equals_streaming() {
256        let candles: Vec<Candle> = (0..40)
257            .map(|i| {
258                let base = 200.0 - i as f64;
259                c(base, base + 0.05, base - 5.0, base - 5.0, i)
260            })
261            .collect();
262        let mut a = ConcealingBabySwallow::new();
263        let mut b = ConcealingBabySwallow::new();
264        assert_eq!(
265            a.batch(&candles),
266            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
267        );
268    }
269
270    #[test]
271    fn reset_clears_state() {
272        let mut t = ConcealingBabySwallow::new();
273        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
274        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
275        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
276        t.update(c(14.0, 14.1, 8.9, 9.0, 3));
277        assert!(t.is_ready());
278        t.reset();
279        assert!(!t.is_ready());
280        assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), None);
281    }
282}