Skip to main content

wickra_core/indicators/
identical_three_crows.rs

1//! Identical Three Crows candlestick pattern.
2
3use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7/// Identical Three Crows — a 3-bar bearish reversal: three consecutive red
8/// candles with steadily lower closes where each candle opens at (or very near)
9/// the prior candle's close, so the bodies stack in an identical staircase.
10///
11/// ```text
12/// tol_n         = tolerance * max(|open|, |prev.close|)
13/// all three red                              (close < open)
14/// declining closes                           (bar2.close < bar1.close, bar3.close < bar2.close)
15/// bar2 opens at bar1's close                 (|bar2.open − bar1.close| <= tol_2)
16/// bar3 opens at bar2's close                 (|bar3.open − bar2.close| <= tol_3)
17/// ```
18///
19/// Output is `−1.0` when the pattern completes and `0.0` otherwise. Identical
20/// Three Crows is a single-direction (bearish-only) pattern, so it never emits
21/// `+1.0`. The first two bars always return `0.0` because the three-bar window
22/// is not yet filled. `tolerance` defaults to `0.001` (10 bps relative) and must
23/// lie in `[0, 1)`. Pattern-shape check only — no trend filter is applied;
24/// combine with a trend indicator for actionable signals.
25///
26/// # Signed ±1 encoding
27///
28/// This detector emits the uniform candlestick sign convention shared across the
29/// pattern family — `−1.0` bearish, `0.0` no pattern — so it drops straight into
30/// a machine-learning feature matrix as a single dimension.
31///
32/// # Example
33///
34/// ```
35/// use wickra_core::{Candle, IdenticalThreeCrows, Indicator};
36///
37/// let mut indicator = IdenticalThreeCrows::new();
38/// indicator.update(Candle::new(13.0, 13.1, 11.9, 12.0, 1.0, 0).unwrap());
39/// indicator.update(Candle::new(12.0, 12.1, 10.9, 11.0, 1.0, 1).unwrap());
40/// let out = indicator
41///     .update(Candle::new(11.0, 11.1, 9.9, 10.0, 1.0, 2).unwrap());
42/// assert_eq!(out, Some(-1.0));
43/// ```
44#[derive(Debug, Clone)]
45pub struct IdenticalThreeCrows {
46    tolerance: f64,
47    prev: Option<Candle>,
48    prev_prev: Option<Candle>,
49    has_emitted: bool,
50}
51
52impl Default for IdenticalThreeCrows {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl IdenticalThreeCrows {
59    /// Construct a detector with the default relative tolerance (1e-3).
60    pub const fn new() -> Self {
61        Self {
62            tolerance: 0.001,
63            prev: None,
64            prev_prev: None,
65            has_emitted: false,
66        }
67    }
68
69    /// Construct a detector with a custom relative tolerance.
70    ///
71    /// `tolerance` must lie in `[0, 1)`.
72    pub fn with_tolerance(tolerance: f64) -> Result<Self> {
73        if !(0.0..1.0).contains(&tolerance) {
74            return Err(Error::InvalidPeriod {
75                message: "identical three crows tolerance must lie in [0, 1)",
76            });
77        }
78        Ok(Self {
79            tolerance,
80            prev: None,
81            prev_prev: None,
82            has_emitted: false,
83        })
84    }
85
86    /// Configured relative tolerance.
87    pub fn tolerance(&self) -> f64 {
88        self.tolerance
89    }
90}
91
92impl Indicator for IdenticalThreeCrows {
93    type Input = Candle;
94    type Output = f64;
95
96    #[inline]
97    fn update(&mut self, candle: Candle) -> Option<f64> {
98        let pp = self.prev_prev;
99        let p = self.prev;
100        self.prev_prev = self.prev;
101        self.prev = Some(candle);
102        let (Some(bar1), Some(bar2)) = (pp, p) else {
103            return None;
104        };
105        self.has_emitted = true;
106        let tol2 = self.tolerance * bar2.open.abs().max(bar1.close.abs());
107        let tol3 = self.tolerance * candle.open.abs().max(bar2.close.abs());
108        if bar1.close < bar1.open
109            && bar2.close < bar2.open
110            && candle.close < candle.open
111            && bar2.close < bar1.close
112            && candle.close < bar2.close
113            && (bar2.open - bar1.close).abs() <= tol2
114            && (candle.open - bar2.close).abs() <= tol3
115        {
116            return Some(-1.0);
117        }
118        Some(0.0)
119    }
120
121    fn reset(&mut self) {
122        self.prev = None;
123        self.prev_prev = None;
124        self.has_emitted = false;
125    }
126
127    #[inline]
128    fn warmup_period(&self) -> usize {
129        3
130    }
131
132    #[inline]
133    fn is_ready(&self) -> bool {
134        self.has_emitted
135    }
136
137    #[inline]
138    fn name(&self) -> &'static str {
139        "IdenticalThreeCrows"
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::traits::BatchExt;
147
148    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
149        Candle::new(open, high, low, close, 1.0, ts).unwrap()
150    }
151
152    #[test]
153    fn rejects_invalid_tolerance() {
154        assert!(IdenticalThreeCrows::with_tolerance(-0.01).is_err());
155        assert!(IdenticalThreeCrows::with_tolerance(1.0).is_err());
156    }
157
158    #[test]
159    fn accepts_valid_tolerance() {
160        let t = IdenticalThreeCrows::with_tolerance(0.0).unwrap();
161        assert!((t.tolerance() - 0.0).abs() < 1e-12);
162    }
163
164    #[test]
165    fn accessors_and_metadata() {
166        let t = IdenticalThreeCrows::default();
167        assert_eq!(t.name(), "IdenticalThreeCrows");
168        assert_eq!(t.warmup_period(), 3);
169        assert!(!t.is_ready());
170        assert!((t.tolerance() - 0.001).abs() < 1e-12);
171    }
172
173    #[test]
174    fn identical_three_crows_is_minus_one() {
175        let mut t = IdenticalThreeCrows::new();
176        // Three red candles, each opening at the prior close, declining.
177        assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), None);
178        assert_eq!(t.update(c(12.0, 12.1, 10.9, 11.0, 1)), None);
179        assert_eq!(t.update(c(11.0, 11.1, 9.9, 10.0, 2)), Some(-1.0));
180    }
181
182    #[test]
183    fn non_identical_opens_yield_zero() {
184        let mut t = IdenticalThreeCrows::new();
185        t.update(c(13.0, 13.1, 11.9, 12.0, 0));
186        t.update(c(12.0, 12.1, 10.9, 11.0, 1));
187        // bar3 opens at 10.0, far from bar2's close (11.0) -> not identical.
188        assert_eq!(t.update(c(10.0, 10.1, 8.9, 9.0, 2)), Some(0.0));
189    }
190
191    #[test]
192    fn rising_close_yields_zero() {
193        let mut t = IdenticalThreeCrows::new();
194        t.update(c(13.0, 13.1, 11.9, 12.0, 0));
195        t.update(c(12.0, 12.1, 10.9, 11.0, 1));
196        // bar3 is green -> not three crows.
197        assert_eq!(t.update(c(11.0, 12.2, 10.9, 12.0, 2)), Some(0.0));
198    }
199
200    #[test]
201    fn first_two_bars_return_zero() {
202        let mut t = IdenticalThreeCrows::new();
203        assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), None);
204        assert_eq!(t.update(c(12.0, 12.1, 10.9, 11.0, 1)), None);
205    }
206
207    #[test]
208    fn batch_equals_streaming() {
209        let candles: Vec<Candle> = (0..40)
210            .map(|i| {
211                let base = 100.0 - i as f64;
212                c(base, base + 0.1, base - 1.1, base - 1.0, i)
213            })
214            .collect();
215        let mut a = IdenticalThreeCrows::new();
216        let mut b = IdenticalThreeCrows::new();
217        assert_eq!(
218            a.batch(&candles),
219            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
220        );
221    }
222
223    #[test]
224    fn reset_clears_state() {
225        let mut t = IdenticalThreeCrows::new();
226        t.update(c(13.0, 13.1, 11.9, 12.0, 0));
227        t.update(c(12.0, 12.1, 10.9, 11.0, 1));
228        t.update(c(11.0, 11.1, 9.9, 10.0, 2));
229        assert!(t.is_ready());
230        t.reset();
231        assert!(!t.is_ready());
232        assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), None);
233    }
234}