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 if lines > crate::error::MAX_PERIOD {
62 return Err(Error::InvalidPeriod {
63 message: crate::error::PERIOD_ABOVE_MAX,
64 });
65 }
66 Ok(Self {
67 lines,
68 line_values: Vec::with_capacity(lines + 1),
69 dir: 0,
70 last: None,
71 })
72 }
73
74 pub const fn lines(&self) -> usize {
76 self.lines
77 }
78
79 pub const fn value(&self) -> Option<f64> {
81 self.last
82 }
83
84 fn push_line(&mut self, close: f64, dir: i8) {
85 self.dir = dir;
86 self.line_values.push(close);
87 if self.line_values.len() > self.lines {
88 self.line_values.remove(0);
89 }
90 }
91}
92
93impl Indicator for ThreeLineBreak {
94 type Input = Candle;
95 type Output = f64;
96
97 #[inline]
98 fn update(&mut self, candle: Candle) -> Option<f64> {
99 let close = candle.close;
100 let Some(&prior) = self.line_values.last() else {
101 self.line_values.push(close);
103 return None;
104 };
105 if self.dir >= 0 {
106 if close > prior {
107 self.push_line(close, 1);
108 } else {
109 let low = self
110 .line_values
111 .iter()
112 .copied()
113 .fold(f64::INFINITY, f64::min);
114 if close < low {
115 self.push_line(close, -1);
116 }
117 }
118 } else if close < prior {
119 self.push_line(close, -1);
120 } else {
121 let high = self
122 .line_values
123 .iter()
124 .copied()
125 .fold(f64::NEG_INFINITY, f64::max);
126 if close > high {
127 self.push_line(close, 1);
128 }
129 }
130 if self.dir == 0 {
131 return None;
132 }
133 let v = f64::from(self.dir);
134 self.last = Some(v);
135 Some(v)
136 }
137
138 fn reset(&mut self) {
139 self.line_values.clear();
140 self.dir = 0;
141 self.last = None;
142 }
143
144 #[inline]
145 fn warmup_period(&self) -> usize {
146 2
147 }
148
149 #[inline]
150 fn is_ready(&self) -> bool {
151 self.last.is_some()
152 }
153
154 #[inline]
155 fn name(&self) -> &'static str {
156 "ThreeLineBreak"
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::traits::BatchExt;
164
165 fn c(close: f64) -> Candle {
166 Candle::new_unchecked(close, close, close, close, 1_000.0, 0)
167 }
168
169 #[test]
170 fn rejects_zero_lines() {
171 assert!(matches!(ThreeLineBreak::new(0), Err(Error::PeriodZero)));
172 }
173
174 #[test]
175 fn accessors_and_metadata() {
176 let t = ThreeLineBreak::new(3).unwrap();
177 assert_eq!(t.lines(), 3);
178 assert_eq!(t.warmup_period(), 2);
179 assert_eq!(t.name(), "ThreeLineBreak");
180 assert!(!t.is_ready());
181 assert_eq!(t.value(), None);
182 }
183
184 #[test]
185 fn uptrend_is_plus_one() {
186 let mut t = ThreeLineBreak::new(3).unwrap();
187 let candles: Vec<Candle> = (0..20).map(|i| c(100.0 + f64::from(i))).collect();
188 let out = t.batch(&candles);
189 assert!(out[0].is_none());
190 assert_eq!(out[1], Some(1.0));
191 assert_eq!(out.last().unwrap(), &Some(1.0));
192 }
193
194 #[test]
195 fn downtrend_is_minus_one() {
196 let mut t = ThreeLineBreak::new(3).unwrap();
197 let candles: Vec<Candle> = (0..20).map(|i| c(100.0 - f64::from(i))).collect();
198 let last = t.batch(&candles).into_iter().flatten().last().unwrap();
199 assert_eq!(last, -1.0);
200 }
201
202 #[test]
203 fn small_pullback_does_not_reverse() {
204 let mut t = ThreeLineBreak::new(3).unwrap();
207 t.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); assert_eq!(t.update(c(102.5)), Some(1.0));
210 }
211
212 #[test]
213 fn break_of_three_line_extreme_reverses() {
214 let mut t = ThreeLineBreak::new(3).unwrap();
215 t.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); assert_eq!(t.update(c(100.5)), Some(-1.0));
218 }
219
220 #[test]
221 fn reset_clears_state() {
222 let mut t = ThreeLineBreak::new(3).unwrap();
223 t.batch(&(0..10).map(|i| c(100.0 + f64::from(i))).collect::<Vec<_>>());
224 assert!(t.is_ready());
225 t.reset();
226 assert!(!t.is_ready());
227 assert_eq!(t.value(), None);
228 assert_eq!(t.update(c(100.0)), None);
229 }
230
231 #[test]
232 fn flat_close_emits_none_until_a_line_forms() {
233 let mut t = ThreeLineBreak::new(3).unwrap();
234 assert_eq!(t.update(c(100.0)), None);
235 assert_eq!(t.update(c(100.0)), None);
237 assert!(!t.is_ready());
238 }
239
240 #[test]
241 fn batch_equals_streaming() {
242 let candles: Vec<Candle> = (0..80)
243 .map(|i| c(100.0 + (f64::from(i) * 0.25).sin() * 9.0))
244 .collect();
245 let batch = ThreeLineBreak::new(3).unwrap().batch(&candles);
246 let mut b = ThreeLineBreak::new(3).unwrap();
247 let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
248 assert_eq!(batch, streamed);
249 }
250}