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        self.has_emitted = true;
70        let bar1 = self.c1;
71        let bar2 = self.c2;
72        let bar3 = self.c3;
73        self.c1 = self.c2;
74        self.c2 = self.c3;
75        self.c3 = Some(candle);
76        let (Some(bar1), Some(bar2), Some(bar3)) = (bar1, bar2, bar3) else {
77            return Some(0.0);
78        };
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    fn warmup_period(&self) -> usize {
122        4
123    }
124
125    fn is_ready(&self) -> bool {
126        self.has_emitted
127    }
128
129    fn name(&self) -> &'static str {
130        "ThreeLineStrike"
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::traits::BatchExt;
138
139    fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
140        Candle::new(open, high, low, close, 1.0, ts).unwrap()
141    }
142
143    #[test]
144    fn accessors_and_metadata() {
145        let t = ThreeLineStrike::new();
146        assert_eq!(t.name(), "ThreeLineStrike");
147        assert_eq!(t.warmup_period(), 4);
148        assert!(!t.is_ready());
149    }
150
151    #[test]
152    fn bullish_three_line_strike_is_plus_one() {
153        let mut t = ThreeLineStrike::new();
154        assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
155        assert_eq!(t.update(c(10.5, 12.1, 10.4, 12.0, 1)), Some(0.0));
156        assert_eq!(t.update(c(11.5, 13.1, 11.4, 13.0, 2)), Some(0.0));
157        assert_eq!(t.update(c(13.5, 13.6, 9.4, 9.5, 3)), Some(1.0));
158    }
159
160    #[test]
161    fn bearish_three_line_strike_is_minus_one() {
162        let mut t = ThreeLineStrike::new();
163        assert_eq!(t.update(c(13.0, 13.1, 11.9, 12.0, 0)), Some(0.0));
164        assert_eq!(t.update(c(12.5, 12.6, 10.9, 11.0, 1)), Some(0.0));
165        assert_eq!(t.update(c(11.5, 11.6, 9.9, 10.0, 2)), Some(0.0));
166        assert_eq!(t.update(c(9.5, 13.6, 9.4, 13.5, 3)), Some(-1.0));
167    }
168
169    #[test]
170    fn strike_not_clearing_first_open_yields_zero() {
171        let mut t = ThreeLineStrike::new();
172        t.update(c(10.0, 11.1, 9.9, 11.0, 0));
173        t.update(c(10.5, 12.1, 10.4, 12.0, 1));
174        t.update(c(11.5, 13.1, 11.4, 13.0, 2));
175        // bar4 closes 10.5, above bar1's open (10.0) -> does not strike through.
176        assert_eq!(t.update(c(13.5, 13.6, 10.4, 10.5, 3)), Some(0.0));
177    }
178
179    #[test]
180    fn first_three_bars_return_zero() {
181        let mut t = ThreeLineStrike::new();
182        assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
183        assert_eq!(t.update(c(10.5, 12.1, 10.4, 12.0, 1)), Some(0.0));
184        assert_eq!(t.update(c(11.5, 13.1, 11.4, 13.0, 2)), Some(0.0));
185    }
186
187    #[test]
188    fn batch_equals_streaming() {
189        let candles: Vec<Candle> = (0..40)
190            .map(|i| {
191                let base = 100.0 + i as f64;
192                c(base, base + 1.5, base - 0.2, base + 1.0, i)
193            })
194            .collect();
195        let mut a = ThreeLineStrike::new();
196        let mut b = ThreeLineStrike::new();
197        assert_eq!(
198            a.batch(&candles),
199            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
200        );
201    }
202
203    #[test]
204    fn reset_clears_state() {
205        let mut t = ThreeLineStrike::new();
206        t.update(c(10.0, 11.1, 9.9, 11.0, 0));
207        t.update(c(10.5, 12.1, 10.4, 12.0, 1));
208        t.update(c(11.5, 13.1, 11.4, 13.0, 2));
209        t.update(c(13.5, 13.6, 9.4, 9.5, 3));
210        assert!(t.is_ready());
211        t.reset();
212        assert!(!t.is_ready());
213        assert_eq!(t.update(c(10.0, 11.1, 9.9, 11.0, 0)), Some(0.0));
214    }
215}