Skip to main content

wickra_core/indicators/
three_line_strike.rs

1//! Three Line Strike candlestick pattern.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// Three Line Strike — a 4-bar pattern: three candles marching in one direction
7/// (a three-soldiers / three-crows advance) followed by a fourth candle of the
8/// opposite colour that opens beyond the third candle and closes back past the
9/// first candle's open, "striking" through the whole run.
10///
11/// **Bullish** (`+1.0`):
12/// ```text
13/// bar1..bar3 green, each opening inside the prior body and closing higher
14/// bar4 red & opens above bar3's close & closes below bar1's open
15/// ```
16///
17/// **Bearish** (`−1.0`): the mirror — three falling red candles struck by a
18/// green bar4 that opens below bar3's close and closes above bar1's open.
19///
20/// Output is `0.0` otherwise. The first three bars always return `0.0` because
21/// the four-bar window is not yet filled. Pattern-shape check only — no trend
22/// filter is applied; combine with a trend indicator for actionable signals.
23///
24/// # Signed ±1 encoding
25///
26/// This detector emits the uniform candlestick sign convention shared across the
27/// pattern family — `+1.0` bullish, `−1.0` bearish, `0.0` no pattern — so it
28/// drops straight into a machine-learning feature matrix where the bullish and
29/// bearish variants occupy a single dimension.
30///
31/// # Example
32///
33/// ```
34/// use wickra_core::{Candle, Indicator, ThreeLineStrike};
35///
36/// let mut indicator = ThreeLineStrike::new();
37/// indicator.update(Candle::new(10.0, 11.1, 9.9, 11.0, 1.0, 0).unwrap());
38/// indicator.update(Candle::new(10.5, 12.1, 10.4, 12.0, 1.0, 1).unwrap());
39/// indicator.update(Candle::new(11.5, 13.1, 11.4, 13.0, 1.0, 2).unwrap());
40/// let out = indicator
41///     .update(Candle::new(13.5, 13.6, 9.4, 9.5, 1.0, 3).unwrap());
42/// assert_eq!(out, Some(1.0));
43/// ```
44#[derive(Debug, Clone, Default)]
45pub struct ThreeLineStrike {
46    c1: Option<Candle>,
47    c2: Option<Candle>,
48    c3: Option<Candle>,
49    has_emitted: bool,
50}
51
52impl ThreeLineStrike {
53    /// Construct a new Three Line Strike detector.
54    pub const fn new() -> Self {
55        Self {
56            c1: None,
57            c2: None,
58            c3: None,
59            has_emitted: false,
60        }
61    }
62}
63
64impl Indicator for ThreeLineStrike {
65    type Input = Candle;
66    type Output = f64;
67
68    fn update(&mut self, candle: Candle) -> Option<f64> {
69        let bar1 = self.c1;
70        let bar2 = self.c2;
71        let bar3 = self.c3;
72        self.c1 = self.c2;
73        self.c2 = self.c3;
74        self.c3 = Some(candle);
75        let (Some(bar1), Some(bar2), Some(bar3)) = (bar1, bar2, bar3) else {
76            return None;
77        };
78        self.has_emitted = true;
79        // Bullish: three rising green candles struck by a red bar4.
80        if bar1.close > bar1.open
81            && bar2.close > bar2.open
82            && bar3.close > bar3.open
83            && bar2.open >= bar1.open
84            && bar2.open <= bar1.close
85            && bar2.close > bar1.close
86            && bar3.open >= bar2.open
87            && bar3.open <= bar2.close
88            && bar3.close > bar2.close
89            && candle.close < candle.open
90            && candle.open > bar3.close
91            && candle.close < bar1.open
92        {
93            return Some(1.0);
94        }
95        // Bearish: three falling red candles struck by a green bar4.
96        if bar1.close < bar1.open
97            && bar2.close < bar2.open
98            && bar3.close < bar3.open
99            && bar2.open <= bar1.open
100            && bar2.open >= bar1.close
101            && bar2.close < bar1.close
102            && bar3.open <= bar2.open
103            && bar3.open >= bar2.close
104            && bar3.close < bar2.close
105            && candle.close > candle.open
106            && candle.open < bar3.close
107            && candle.close > bar1.open
108        {
109            return Some(-1.0);
110        }
111        Some(0.0)
112    }
113
114    fn reset(&mut self) {
115        self.c1 = None;
116        self.c2 = None;
117        self.c3 = None;
118        self.has_emitted = false;
119    }
120
121    #[inline]
122    fn warmup_period(&self) -> usize {
123        4
124    }
125
126    #[inline]
127    fn is_ready(&self) -> bool {
128        self.has_emitted
129    }
130
131    #[inline]
132    fn name(&self) -> &'static str {
133        "ThreeLineStrike"
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use crate::traits::BatchExt;
141
142    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
143        Candle::new(open, high, low, close, 1.0, ts).unwrap()
144    }
145
146    #[test]
147    fn accessors_and_metadata() {
148        let t = ThreeLineStrike::new();
149        assert_eq!(t.name(), "ThreeLineStrike");
150        assert_eq!(t.warmup_period(), 4);
151        assert!(!t.is_ready());
152    }
153
154    #[test]
155    fn bullish_three_line_strike_is_plus_one() {
156        let mut t = ThreeLineStrike::new();
157        assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), None);
158        assert_eq!(t.update(c(10.5, 12.1, 10.4, 12.0, 1)), None);
159        assert_eq!(t.update(c(11.5, 13.1, 11.4, 13.0, 2)), None);
160        assert_eq!(t.update(c(13.5, 13.6, 9.4, 9.5, 3)), Some(1.0));
161    }
162
163    #[test]
164    fn bearish_three_line_strike_is_minus_one() {
165        let mut t = ThreeLineStrike::new();
166        assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), None);
167        assert_eq!(t.update(c(12.5, 12.6, 10.9, 11.0, 1)), None);
168        assert_eq!(t.update(c(11.5, 11.6, 9.9, 10.0, 2)), None);
169        assert_eq!(t.update(c(9.5, 13.6, 9.4, 13.5, 3)), Some(-1.0));
170    }
171
172    #[test]
173    fn strike_not_clearing_first_open_yields_zero() {
174        let mut t = ThreeLineStrike::new();
175        t.update(c(10.0, 11.1, 9.9, 11.0, 0));
176        t.update(c(10.5, 12.1, 10.4, 12.0, 1));
177        t.update(c(11.5, 13.1, 11.4, 13.0, 2));
178        // bar4 closes 10.5, above bar1's open (10.0) -> does not strike through.
179        assert_eq!(t.update(c(13.5, 13.6, 10.4, 10.5, 3)), Some(0.0));
180    }
181
182    #[test]
183    fn first_three_bars_return_zero() {
184        let mut t = ThreeLineStrike::new();
185        assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), None);
186        assert_eq!(t.update(c(10.5, 12.1, 10.4, 12.0, 1)), None);
187        assert_eq!(t.update(c(11.5, 13.1, 11.4, 13.0, 2)), None);
188    }
189
190    #[test]
191    fn batch_equals_streaming() {
192        let candles: Vec<Candle> = (0..40)
193            .map(|i| {
194                let base = 100.0 + i as f64;
195                c(base, base + 1.5, base - 0.2, base + 1.0, i)
196            })
197            .collect();
198        let mut a = ThreeLineStrike::new();
199        let mut b = ThreeLineStrike::new();
200        assert_eq!(
201            a.batch(&candles),
202            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
203        );
204    }
205
206    #[test]
207    fn reset_clears_state() {
208        let mut t = ThreeLineStrike::new();
209        t.update(c(10.0, 11.1, 9.9, 11.0, 0));
210        t.update(c(10.5, 12.1, 10.4, 12.0, 1));
211        t.update(c(11.5, 13.1, 11.4, 13.0, 2));
212        t.update(c(13.5, 13.6, 9.4, 9.5, 3));
213        assert!(t.is_ready());
214        t.reset();
215        assert!(!t.is_ready());
216        assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), None);
217    }
218}