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    fn update(&mut self, candle: Candle) -> Option<f64> {
82        self.has_emitted = true;
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 Some(0.0);
91        };
92        // bar1 and bar2 are black marubozu.
93        if !black_marubozu(bar1) || !black_marubozu(bar2) {
94            return Some(0.0);
95        }
96        // bar3 is black, gaps down on the open, throws an upper shadow into bar2.
97        if bar3.open <= bar3.close {
98            return Some(0.0);
99        }
100        if bar3.open >= bar2.close {
101            return Some(0.0); // no downside open gap
102        }
103        if bar3.high <= bar2.close {
104            return Some(0.0); // upper shadow does not reach into bar2's body
105        }
106        // bar4 is black and engulfs bar3 including its shadows.
107        if candle.open <= candle.close {
108            return Some(0.0);
109        }
110        if candle.open > bar3.high && candle.close < bar3.low {
111            return Some(1.0);
112        }
113        Some(0.0)
114    }
115
116    fn reset(&mut self) {
117        self.c1 = None;
118        self.c2 = None;
119        self.c3 = None;
120        self.has_emitted = false;
121    }
122
123    fn warmup_period(&self) -> usize {
124        4
125    }
126
127    fn is_ready(&self) -> bool {
128        self.has_emitted
129    }
130
131    fn name(&self) -> &'static str {
132        "ConcealingBabySwallow"
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::traits::BatchExt;
140
141    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
142        Candle::new(open, high, low, close, 1.0, ts).unwrap()
143    }
144
145    #[test]
146    fn accessors_and_metadata() {
147        let t = ConcealingBabySwallow::new();
148        assert_eq!(t.name(), "ConcealingBabySwallow");
149        assert_eq!(t.warmup_period(), 4);
150        assert!(!t.is_ready());
151    }
152
153    #[test]
154    fn concealing_baby_swallow_is_plus_one() {
155        let mut t = ConcealingBabySwallow::new();
156        assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
157        assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 1)), Some(0.0));
158        assert_eq!(t.update(c(11.0, 13.0, 9.9, 10.0, 2)), Some(0.0));
159        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(1.0));
160    }
161
162    #[test]
163    fn warmup_returns_zero() {
164        let mut t = ConcealingBabySwallow::new();
165        assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
166        assert_eq!(t.update(c(16.0, 16.1, 11.9, 12.0, 1)), Some(0.0));
167        assert_eq!(t.update(c(11.0, 13.0, 9.9, 10.0, 2)), Some(0.0));
168    }
169
170    #[test]
171    fn first_bar_not_marubozu_yields_zero() {
172        let mut t = ConcealingBabySwallow::new();
173        // bar1 white.
174        t.update(c(15.0, 20.1, 14.9, 20.0, 0));
175        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
176        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
177        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
178    }
179
180    #[test]
181    fn first_bar_zero_range_yields_zero() {
182        let mut t = ConcealingBabySwallow::new();
183        // bar1 zero range -> not a marubozu.
184        t.update(c(15.0, 15.0, 15.0, 15.0, 0));
185        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
186        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
187        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
188    }
189
190    #[test]
191    fn second_bar_not_marubozu_yields_zero() {
192        let mut t = ConcealingBabySwallow::new();
193        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
194        // bar2 white.
195        t.update(c(12.0, 16.1, 11.9, 16.0, 1));
196        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
197        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
198    }
199
200    #[test]
201    fn third_bar_not_black_yields_zero() {
202        let mut t = ConcealingBabySwallow::new();
203        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
204        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
205        // bar3 white.
206        t.update(c(11.0, 13.0, 9.9, 12.5, 2));
207        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
208    }
209
210    #[test]
211    fn third_bar_no_gap_yields_zero() {
212        let mut t = ConcealingBabySwallow::new();
213        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
214        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
215        // bar3 black but opens at/above bar2's close -> no downside gap.
216        t.update(c(12.5, 13.0, 9.9, 10.0, 2));
217        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
218    }
219
220    #[test]
221    fn third_bar_no_upper_shadow_yields_zero() {
222        let mut t = ConcealingBabySwallow::new();
223        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
224        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
225        // bar3 black, gaps down, but its high does not reach into bar2's body.
226        t.update(c(11.0, 11.5, 9.9, 10.0, 2));
227        assert_eq!(t.update(c(14.0, 14.1, 8.9, 9.0, 3)), Some(0.0));
228    }
229
230    #[test]
231    fn fourth_bar_not_black_yields_zero() {
232        let mut t = ConcealingBabySwallow::new();
233        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
234        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
235        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
236        // bar4 white.
237        assert_eq!(t.update(c(14.0, 14.1, 8.9, 14.05, 3)), Some(0.0));
238    }
239
240    #[test]
241    fn fourth_bar_not_engulfing_yields_zero() {
242        let mut t = ConcealingBabySwallow::new();
243        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
244        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
245        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
246        // bar4 black but does not engulf bar3's high.
247        assert_eq!(t.update(c(12.5, 12.6, 8.9, 9.0, 3)), Some(0.0));
248    }
249
250    #[test]
251    fn batch_equals_streaming() {
252        let candles: Vec<Candle> = (0..40)
253            .map(|i| {
254                let base = 200.0 - i as f64;
255                c(base, base + 0.05, base - 5.0, base - 5.0, i)
256            })
257            .collect();
258        let mut a = ConcealingBabySwallow::new();
259        let mut b = ConcealingBabySwallow::new();
260        assert_eq!(
261            a.batch(&candles),
262            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
263        );
264    }
265
266    #[test]
267    fn reset_clears_state() {
268        let mut t = ConcealingBabySwallow::new();
269        t.update(c(20.0, 20.1, 14.9, 15.0, 0));
270        t.update(c(16.0, 16.1, 11.9, 12.0, 1));
271        t.update(c(11.0, 13.0, 9.9, 10.0, 2));
272        t.update(c(14.0, 14.1, 8.9, 9.0, 3));
273        assert!(t.is_ready());
274        t.reset();
275        assert!(!t.is_ready());
276        assert_eq!(t.update(c(20.0, 20.1, 14.9, 15.0, 0)), Some(0.0));
277    }
278}