Skip to main content

wickra_core/indicators/
three_inside.rs

1//! Three Inside Up / Down candlestick pattern.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Three Inside Up / Down — a confirmed Harami: the first two bars form a
7/// Harami and the third bar confirms direction by closing beyond the first
8/// bar's body.
9///
10/// **Three Inside Up** (`+1.0`):
11/// 1. Bar 1 is a long red candle.
12/// 2. Bar 2 is a small green candle whose body sits inside Bar 1's body.
13/// 3. Bar 3 is a green candle whose close exceeds Bar 1's open.
14///
15/// **Three Inside Down** (`−1.0`): the mirror — long green, small red inside,
16/// red closing below Bar 1's open.
17///
18/// Pattern-shape check only — no trend filter is applied; combine with a trend
19/// indicator for actionable signals.
20///
21/// # Example
22///
23/// ```
24/// use wickra_core::{Candle, Indicator, ThreeInside};
25///
26/// let mut indicator = ThreeInside::new();
27/// indicator.update(Candle::new(12.0, 12.5, 9.5, 10.0, 1.0, 0).unwrap());
28/// indicator.update(Candle::new(10.5, 11.5, 10.4, 11.0, 1.0, 1).unwrap());
29/// let out = indicator
30///     .update(Candle::new(11.0, 13.0, 10.9, 12.5, 1.0, 2).unwrap());
31/// assert_eq!(out, Some(1.0));
32/// ```
33#[derive(Debug, Clone, Default)]
34pub struct ThreeInside {
35    prev: Option<Candle>,
36    prev_prev: Option<Candle>,
37    has_emitted: bool,
38}
39
40impl ThreeInside {
41    /// Construct a new Three Inside Up / Down detector.
42    pub const fn new() -> Self {
43        Self {
44            prev: None,
45            prev_prev: None,
46            has_emitted: false,
47        }
48    }
49}
50
51impl Indicator for ThreeInside {
52    type Input = Candle;
53    type Output = f64;
54
55    fn update(&mut self, candle: Candle) -> Option<f64> {
56        self.has_emitted = true;
57        let pp = self.prev_prev;
58        let p = self.prev;
59        self.prev_prev = self.prev;
60        self.prev = Some(candle);
61        let (Some(b1), Some(b2)) = (pp, p) else {
62            return Some(0.0);
63        };
64        let body1 = (b1.close - b1.open).abs();
65        let body2 = (b2.close - b2.open).abs();
66        if body1 <= 0.0 || body2 <= 0.0 || body2 >= body1 {
67            return Some(0.0);
68        }
69        let b1_red = b1.close < b1.open;
70        let b1_green = b1.close > b1.open;
71        let b2_green = b2.close > b2.open;
72        let b2_red = b2.close < b2.open;
73        let b3_green = candle.close > candle.open;
74        let b3_red = candle.close < candle.open;
75        // Bullish: prior red, inside green harami, then green confirms above b1.open.
76        if b1_red
77            && b2_green
78            && b2.open >= b1.close
79            && b2.close <= b1.open
80            && b3_green
81            && candle.close > b1.open
82        {
83            return Some(1.0);
84        }
85        // Bearish: prior green, inside red harami, then red confirms below b1.open.
86        if b1_green
87            && b2_red
88            && b2.open <= b1.close
89            && b2.close >= b1.open
90            && b3_red
91            && candle.close < b1.open
92        {
93            return Some(-1.0);
94        }
95        Some(0.0)
96    }
97
98    fn reset(&mut self) {
99        self.prev = None;
100        self.prev_prev = None;
101        self.has_emitted = false;
102    }
103
104    fn warmup_period(&self) -> usize {
105        3
106    }
107
108    fn is_ready(&self) -> bool {
109        self.has_emitted
110    }
111
112    fn name(&self) -> &'static str {
113        "ThreeInside"
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::traits::BatchExt;
121
122    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
123        Candle::new(open, high, low, close, 1.0, ts).unwrap()
124    }
125
126    #[test]
127    fn accessors_and_metadata() {
128        let t = ThreeInside::new();
129        assert_eq!(t.name(), "ThreeInside");
130        assert_eq!(t.warmup_period(), 3);
131        assert!(!t.is_ready());
132    }
133
134    #[test]
135    fn three_inside_up_is_plus_one() {
136        let mut t = ThreeInside::new();
137        assert_eq!(t.update(c(12.0, 12.5, 9.5, 10.0, 0)), Some(0.0));
138        assert_eq!(t.update(c(10.5, 11.5, 10.4, 11.0, 1)), Some(0.0));
139        // Bar 3 closes 12.5 > Bar 1 open 12.
140        assert_eq!(t.update(c(11.0, 13.0, 10.9, 12.5, 2)), Some(1.0));
141    }
142
143    #[test]
144    fn three_inside_down_is_minus_one() {
145        let mut t = ThreeInside::new();
146        assert_eq!(t.update(c(10.0, 12.5, 9.5, 12.0, 0)), Some(0.0));
147        assert_eq!(t.update(c(11.5, 11.6, 10.9, 11.0, 1)), Some(0.0));
148        // Bar 3 closes 9.5 < Bar 1 open 10.
149        assert_eq!(t.update(c(11.0, 11.1, 9.3, 9.5, 2)), Some(-1.0));
150    }
151
152    #[test]
153    fn unconfirmed_third_bar_yields_zero() {
154        let mut t = ThreeInside::new();
155        t.update(c(12.0, 12.5, 9.5, 10.0, 0));
156        t.update(c(10.5, 11.5, 10.4, 11.0, 1));
157        // Bar 3 green but closes below b1.open (12).
158        assert_eq!(t.update(c(11.0, 11.8, 10.9, 11.5, 2)), Some(0.0));
159    }
160
161    #[test]
162    fn first_two_bars_return_zero() {
163        let mut t = ThreeInside::new();
164        assert_eq!(t.update(c(12.0, 12.5, 9.5, 10.0, 0)), Some(0.0));
165        assert_eq!(t.update(c(10.5, 11.5, 10.4, 11.0, 1)), Some(0.0));
166    }
167
168    #[test]
169    fn batch_equals_streaming() {
170        let candles: Vec<Candle> = (0..40)
171            .map(|i| {
172                let base = 100.0 + i as f64;
173                c(base, base + 1.0, base - 0.5, base + 0.5, i)
174            })
175            .collect();
176        let mut a = ThreeInside::new();
177        let mut b = ThreeInside::new();
178        assert_eq!(
179            a.batch(&candles),
180            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
181        );
182    }
183
184    #[test]
185    fn reset_clears_state() {
186        let mut t = ThreeInside::new();
187        t.update(c(12.0, 12.5, 9.5, 10.0, 0));
188        t.update(c(10.5, 11.5, 10.4, 11.0, 1));
189        t.update(c(11.0, 13.0, 10.9, 12.5, 2));
190        assert!(t.is_ready());
191        t.reset();
192        assert!(!t.is_ready());
193        assert_eq!(t.update(c(12.0, 12.5, 9.5, 10.0, 0)), Some(0.0));
194    }
195}