Skip to main content

wickra_core/indicators/
three_line_break.rs

1//! Three Line Break — the close-driven line-break chart trend, as a direction.
2
3use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7/// Three Line Break — the trend direction of a line-break ("kakushi") chart, where
8/// a reversal requires the close to break the extreme of the last `lines` lines.
9///
10/// ```text
11/// continue the trend when close exceeds the prior line's end
12/// reverse the trend when close breaks beyond the extreme of the last `lines` lines
13/// output = current line direction: +1 (up), −1 (down)
14/// ```
15///
16/// A line-break chart ignores time and small moves entirely: it draws a new line
17/// only when the close makes a new extreme in the trend, and flips direction only
18/// when the close reverses past the high (or low) of the last `lines` lines —
19/// classically **three**. This filters out minor pullbacks, so the emitted
20/// direction stays in a trend until a genuinely significant reversal. Distinct from
21/// the candlestick [`ThreeLineStrike`](crate::ThreeLineStrike) (a fixed four-bar
22/// pattern); this is the line-break *chart type* reduced to its trend state. See
23/// also the alt-chart "Three-Line-Break Bars" builder.
24///
25/// The output is `+1.0` / `−1.0`. The first bar seeds the reference price; the
26/// direction is emitted once the first line is drawn (data-dependent;
27/// `warmup_period` returns the minimum `2`). Each `update` is O(`lines`).
28///
29/// # Example
30///
31/// ```
32/// use wickra_core::{Candle, Indicator, ThreeLineBreak};
33///
34/// let mut indicator = ThreeLineBreak::new(3).unwrap();
35/// let mut last = None;
36/// for i in 0..20 {
37///     let close = 100.0 + f64::from(i);
38///     let c = Candle::new(close, close, close, close, 1_000.0, 0).unwrap();
39///     last = indicator.update(c);
40/// }
41/// assert_eq!(last, Some(1.0));
42/// ```
43#[derive(Debug, Clone)]
44pub struct ThreeLineBreak {
45    lines: usize,
46    line_values: Vec<f64>,
47    dir: i8,
48    last: Option<f64>,
49}
50
51impl ThreeLineBreak {
52    /// Construct a Three Line Break requiring `lines` lines to reverse (classic 3).
53    ///
54    /// # Errors
55    ///
56    /// Returns [`Error::PeriodZero`] if `lines == 0`.
57    pub fn new(lines: usize) -> Result<Self> {
58        if lines == 0 {
59            return Err(Error::PeriodZero);
60        }
61        Ok(Self {
62            lines,
63            line_values: Vec::with_capacity(lines + 1),
64            dir: 0,
65            last: None,
66        })
67    }
68
69    /// Configured number of lines required to reverse.
70    pub const fn lines(&self) -> usize {
71        self.lines
72    }
73
74    /// Current direction if available.
75    pub const fn value(&self) -> Option<f64> {
76        self.last
77    }
78
79    fn push_line(&mut self, close: f64, dir: i8) {
80        self.dir = dir;
81        self.line_values.push(close);
82        if self.line_values.len() > self.lines {
83            self.line_values.remove(0);
84        }
85    }
86}
87
88impl Indicator for ThreeLineBreak {
89    type Input = Candle;
90    type Output = f64;
91
92    fn update(&mut self, candle: Candle) -> Option<f64> {
93        let close = candle.close;
94        let Some(&prior) = self.line_values.last() else {
95            // Seed the reference price; no line yet.
96            self.line_values.push(close);
97            return None;
98        };
99        if self.dir >= 0 {
100            if close > prior {
101                self.push_line(close, 1);
102            } else {
103                let low = self
104                    .line_values
105                    .iter()
106                    .copied()
107                    .fold(f64::INFINITY, f64::min);
108                if close < low {
109                    self.push_line(close, -1);
110                }
111            }
112        } else if close < prior {
113            self.push_line(close, -1);
114        } else {
115            let high = self
116                .line_values
117                .iter()
118                .copied()
119                .fold(f64::NEG_INFINITY, f64::max);
120            if close > high {
121                self.push_line(close, 1);
122            }
123        }
124        if self.dir == 0 {
125            return None;
126        }
127        let v = f64::from(self.dir);
128        self.last = Some(v);
129        Some(v)
130    }
131
132    fn reset(&mut self) {
133        self.line_values.clear();
134        self.dir = 0;
135        self.last = None;
136    }
137
138    fn warmup_period(&self) -> usize {
139        2
140    }
141
142    fn is_ready(&self) -> bool {
143        self.last.is_some()
144    }
145
146    fn name(&self) -> &'static str {
147        "ThreeLineBreak"
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154    use crate::traits::BatchExt;
155
156    fn c(close: f64) -> Candle {
157        Candle::new_unchecked(close, close, close, close, 1_000.0, 0)
158    }
159
160    #[test]
161    fn rejects_zero_lines() {
162        assert!(matches!(ThreeLineBreak::new(0), Err(Error::PeriodZero)));
163    }
164
165    #[test]
166    fn accessors_and_metadata() {
167        let t = ThreeLineBreak::new(3).unwrap();
168        assert_eq!(t.lines(), 3);
169        assert_eq!(t.warmup_period(), 2);
170        assert_eq!(t.name(), "ThreeLineBreak");
171        assert!(!t.is_ready());
172        assert_eq!(t.value(), None);
173    }
174
175    #[test]
176    fn uptrend_is_plus_one() {
177        let mut t = ThreeLineBreak::new(3).unwrap();
178        let candles: Vec<Candle> = (0..20).map(|i| c(100.0 + f64::from(i))).collect();
179        let out = t.batch(&candles);
180        assert!(out[0].is_none());
181        assert_eq!(out[1], Some(1.0));
182        assert_eq!(out.last().unwrap(), &Some(1.0));
183    }
184
185    #[test]
186    fn downtrend_is_minus_one() {
187        let mut t = ThreeLineBreak::new(3).unwrap();
188        let candles: Vec<Candle> = (0..20).map(|i| c(100.0 - f64::from(i))).collect();
189        let last = t.batch(&candles).into_iter().flatten().last().unwrap();
190        assert_eq!(last, -1.0);
191    }
192
193    #[test]
194    fn small_pullback_does_not_reverse() {
195        // Rise to build 3 up-lines, then a small dip that does not break the
196        // 3-line low keeps the direction up.
197        let mut t = ThreeLineBreak::new(3).unwrap();
198        t.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); // up-lines at 101,102,103
199                                                            // close 102.5 is below the prior line (103) but above the 3-line low (101) -> no reversal.
200        assert_eq!(t.update(c(102.5)), Some(1.0));
201    }
202
203    #[test]
204    fn break_of_three_line_extreme_reverses() {
205        let mut t = ThreeLineBreak::new(3).unwrap();
206        t.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); // lines 101,102,103, dir up
207                                                            // close 100.5 breaks below the 3-line low (101) -> reverse to down.
208        assert_eq!(t.update(c(100.5)), Some(-1.0));
209    }
210
211    #[test]
212    fn reset_clears_state() {
213        let mut t = ThreeLineBreak::new(3).unwrap();
214        t.batch(&(0..10).map(|i| c(100.0 + f64::from(i))).collect::<Vec<_>>());
215        assert!(t.is_ready());
216        t.reset();
217        assert!(!t.is_ready());
218        assert_eq!(t.value(), None);
219        assert_eq!(t.update(c(100.0)), None);
220    }
221
222    #[test]
223    fn flat_close_emits_none_until_a_line_forms() {
224        let mut t = ThreeLineBreak::new(3).unwrap();
225        assert_eq!(t.update(c(100.0)), None);
226        // An identical close draws no line, so the direction stays unset.
227        assert_eq!(t.update(c(100.0)), None);
228        assert!(!t.is_ready());
229    }
230
231    #[test]
232    fn batch_equals_streaming() {
233        let candles: Vec<Candle> = (0..80)
234            .map(|i| c(100.0 + (f64::from(i) * 0.25).sin() * 9.0))
235            .collect();
236        let batch = ThreeLineBreak::new(3).unwrap().batch(&candles);
237        let mut b = ThreeLineBreak::new(3).unwrap();
238        let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
239        assert_eq!(batch, streamed);
240    }
241}