wickra_core/indicators/
three_line_break.rs1use crate::error::{Error, Result};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[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 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 pub const fn lines(&self) -> usize {
71 self.lines
72 }
73
74 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 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 let mut t = ThreeLineBreak::new(3).unwrap();
198 t.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); 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)]); 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 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}