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    fn update(&mut self, candle: Candle) -> Option<f64> {
97        self.has_emitted = true;
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 Some(0.0);
104        };
105        let tol2 = self.tolerance * bar2.open.abs().max(bar1.close.abs());
106        let tol3 = self.tolerance * candle.open.abs().max(bar2.close.abs());
107        if bar1.close < bar1.open
108            && bar2.close < bar2.open
109            && candle.close < candle.open
110            && bar2.close < bar1.close
111            && candle.close < bar2.close
112            && (bar2.open - bar1.close).abs() <= tol2
113            && (candle.open - bar2.close).abs() <= tol3
114        {
115            return Some(-1.0);
116        }
117        Some(0.0)
118    }
119
120    fn reset(&mut self) {
121        self.prev = None;
122        self.prev_prev = None;
123        self.has_emitted = false;
124    }
125
126    fn warmup_period(&self) -> usize {
127        3
128    }
129
130    fn is_ready(&self) -> bool {
131        self.has_emitted
132    }
133
134    fn name(&self) -> &'static str {
135        "IdenticalThreeCrows"
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::traits::BatchExt;
143
144    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
145        Candle::new(open, high, low, close, 1.0, ts).unwrap()
146    }
147
148    #[test]
149    fn rejects_invalid_tolerance() {
150        assert!(IdenticalThreeCrows::with_tolerance(-0.01).is_err());
151        assert!(IdenticalThreeCrows::with_tolerance(1.0).is_err());
152    }
153
154    #[test]
155    fn accepts_valid_tolerance() {
156        let t = IdenticalThreeCrows::with_tolerance(0.0).unwrap();
157        assert!((t.tolerance() - 0.0).abs() < 1e-12);
158    }
159
160    #[test]
161    fn accessors_and_metadata() {
162        let t = IdenticalThreeCrows::default();
163        assert_eq!(t.name(), "IdenticalThreeCrows");
164        assert_eq!(t.warmup_period(), 3);
165        assert!(!t.is_ready());
166        assert!((t.tolerance() - 0.001).abs() < 1e-12);
167    }
168
169    #[test]
170    fn identical_three_crows_is_minus_one() {
171        let mut t = IdenticalThreeCrows::new();
172        // Three red candles, each opening at the prior close, declining.
173        assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
174        assert_eq!(t.update(c(12.0, 12.1, 10.9, 11.0, 1)), Some(0.0));
175        assert_eq!(t.update(c(11.0, 11.1, 9.9, 10.0, 2)), Some(-1.0));
176    }
177
178    #[test]
179    fn non_identical_opens_yield_zero() {
180        let mut t = IdenticalThreeCrows::new();
181        t.update(c(13.0, 13.1, 11.9, 12.0, 0));
182        t.update(c(12.0, 12.1, 10.9, 11.0, 1));
183        // bar3 opens at 10.0, far from bar2's close (11.0) -> not identical.
184        assert_eq!(t.update(c(10.0, 10.1, 8.9, 9.0, 2)), Some(0.0));
185    }
186
187    #[test]
188    fn rising_close_yields_zero() {
189        let mut t = IdenticalThreeCrows::new();
190        t.update(c(13.0, 13.1, 11.9, 12.0, 0));
191        t.update(c(12.0, 12.1, 10.9, 11.0, 1));
192        // bar3 is green -> not three crows.
193        assert_eq!(t.update(c(11.0, 12.2, 10.9, 12.0, 2)), Some(0.0));
194    }
195
196    #[test]
197    fn first_two_bars_return_zero() {
198        let mut t = IdenticalThreeCrows::new();
199        assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
200        assert_eq!(t.update(c(12.0, 12.1, 10.9, 11.0, 1)), Some(0.0));
201    }
202
203    #[test]
204    fn batch_equals_streaming() {
205        let candles: Vec<Candle> = (0..40)
206            .map(|i| {
207                let base = 100.0 - i as f64;
208                c(base, base + 0.1, base - 1.1, base - 1.0, i)
209            })
210            .collect();
211        let mut a = IdenticalThreeCrows::new();
212        let mut b = IdenticalThreeCrows::new();
213        assert_eq!(
214            a.batch(&candles),
215            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
216        );
217    }
218
219    #[test]
220    fn reset_clears_state() {
221        let mut t = IdenticalThreeCrows::new();
222        t.update(c(13.0, 13.1, 11.9, 12.0, 0));
223        t.update(c(12.0, 12.1, 10.9, 11.0, 1));
224        t.update(c(11.0, 11.1, 9.9, 10.0, 2));
225        assert!(t.is_ready());
226        t.reset();
227        assert!(!t.is_ready());
228        assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
229    }
230}