1use super::PriceBasedSource;
13use super::PriceBuilder;
14use super::bar::Bar;
15use chrono::{DateTime, Utc};
16
17#[derive(Debug, Clone)]
22pub struct LineBreakConfig {
23 pub line_count: usize,
25 pub source: PriceBasedSource,
27}
28
29impl Default for LineBreakConfig {
30 fn default() -> Self {
31 Self {
32 line_count: 3,
33 source: PriceBasedSource::Close,
34 }
35 }
36}
37
38impl LineBreakConfig {
39 pub fn new(line_count: usize) -> Self {
41 Self {
42 line_count,
43 source: PriceBasedSource::Close,
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum LineDirection {
51 Up,
53 Down,
55}
56
57#[derive(Debug, Clone)]
62pub struct LineBreakLine {
63 pub open: f64,
65 pub close: f64,
67 pub direction: LineDirection,
69 pub ts: DateTime<Utc>,
71}
72
73impl LineBreakLine {
74 pub fn high(&self) -> f64 {
76 self.open.max(self.close)
77 }
78
79 pub fn low(&self) -> f64 {
81 self.open.min(self.close)
82 }
83
84 pub fn is_bullish(&self) -> bool {
86 self.direction == LineDirection::Up
87 }
88
89 pub fn is_bearish(&self) -> bool {
91 self.direction == LineDirection::Down
92 }
93}
94
95pub fn to_line_break_lines(data: &[Bar], config: &LineBreakConfig) -> Vec<LineBreakLine> {
100 let mut builder = LineBreakBuilder::new(config);
101 for bar in data {
102 builder.push(bar);
103 }
104 builder.elements()
105}
106
107pub(crate) const MAX_LINES: usize = 10_000;
108
109#[derive(Clone)]
111pub(crate) struct LineBreakBuilder {
112 line_count: usize,
113 source: PriceBasedSource,
114 lines: Vec<LineBreakLine>,
115}
116
117impl LineBreakBuilder {
118 pub(crate) fn new(config: &LineBreakConfig) -> Self {
119 Self {
120 line_count: config.line_count,
121 source: config.source,
122 lines: Vec::new(),
123 }
124 }
125}
126
127impl PriceBuilder for LineBreakBuilder {
128 type Element = LineBreakLine;
129
130 fn push(&mut self, bar: &Bar) {
131 let Some(last_line) = self.lines.last() else {
132 self.lines.push(LineBreakLine {
134 open: bar.open,
135 close: bar.close,
136 direction: if bar.close >= bar.open {
137 LineDirection::Up
138 } else {
139 LineDirection::Down
140 },
141 ts: bar.time,
142 });
143 return;
144 };
145 let lines = &self.lines;
146 let curr_direction = last_line.direction;
147
148 let price = match self.source {
149 PriceBasedSource::Close => bar.close,
150 PriceBasedSource::HighLow if curr_direction == LineDirection::Up => {
151 if bar.high > last_line.high() {
152 bar.high
153 } else {
154 bar.low
155 }
156 }
157 PriceBasedSource::HighLow => {
158 if bar.low < last_line.low() {
159 bar.low
160 } else {
161 bar.high
162 }
163 }
164 };
165 let new_line = match curr_direction {
166 LineDirection::Up => {
167 if price > last_line.high() {
169 Some(LineBreakLine {
170 open: last_line.close,
171 close: price,
172 direction: LineDirection::Up,
173 ts: bar.time,
174 })
175 }
176 else {
178 let lookback = self.line_count.min(lines.len());
179 let reversal_low = lines[lines.len() - lookback..]
180 .iter()
181 .map(|l| l.low())
182 .fold(f64::INFINITY, f64::min);
183
184 (price < reversal_low).then_some(LineBreakLine {
185 open: last_line.open,
186 close: price,
187 direction: LineDirection::Down,
188 ts: bar.time,
189 })
190 }
191 }
192 LineDirection::Down => {
193 if price < last_line.low() {
195 Some(LineBreakLine {
196 open: last_line.close,
197 close: price,
198 direction: LineDirection::Down,
199 ts: bar.time,
200 })
201 }
202 else {
204 let lookback = self.line_count.min(lines.len());
205 let reversal_high = lines[lines.len() - lookback..]
206 .iter()
207 .map(|l| l.high())
208 .fold(f64::NEG_INFINITY, f64::max);
209
210 (price > reversal_high).then_some(LineBreakLine {
211 open: last_line.open,
212 close: price,
213 direction: LineDirection::Up,
214 ts: bar.time,
215 })
216 }
217 }
218 };
219 self.lines.extend(new_line);
220
221 let keep = MAX_LINES.max(self.line_count);
223 if self.lines.len() >= keep.saturating_mul(2) {
224 self.lines.drain(..self.lines.len() - keep);
225 }
226 }
227
228 fn elements(&self) -> Vec<LineBreakLine> {
229 self.lines[self.lines.len().saturating_sub(MAX_LINES)..].to_vec()
230 }
231}
232
233#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub enum LineBreakSignal {
236 Bullish,
238 Bearish,
240 BullishReversal,
242 BearishReversal,
244 None,
246}
247
248pub fn detect_signal(lines: &[LineBreakLine]) -> LineBreakSignal {
252 if lines.len() < 2 {
253 return LineBreakSignal::None;
254 }
255
256 let prev = &lines[lines.len() - 2];
257 let current = &lines[lines.len() - 1];
258
259 match (prev.direction, current.direction) {
260 (LineDirection::Down, LineDirection::Up) => LineBreakSignal::BullishReversal,
261 (LineDirection::Up, LineDirection::Down) => LineBreakSignal::BearishReversal,
262 (LineDirection::Up, LineDirection::Up) => LineBreakSignal::Bullish,
263 (LineDirection::Down, LineDirection::Down) => LineBreakSignal::Bearish,
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 #[test]
272 fn test_signal_detection() {
273 let lines = vec![
274 LineBreakLine {
275 open: 100.0,
276 close: 105.0,
277 direction: LineDirection::Up,
278 ts: Utc::now(),
279 },
280 LineBreakLine {
281 open: 105.0,
282 close: 98.0,
283 direction: LineDirection::Down,
284 ts: Utc::now(),
285 },
286 ];
287
288 let signal = detect_signal(&lines);
289 assert_eq!(signal, LineBreakSignal::BearishReversal);
290 }
291
292 #[test]
293 fn reversal_lines_open_at_the_last_line_open() {
294 let rows = [
296 (
297 10.0,
298 [11.0, 12.0, 13.0, 9.0],
299 [(10.0, 11.0), (11.0, 12.0), (12.0, 13.0), (12.0, 9.0)],
300 ),
301 (
302 13.0,
303 [12.0, 11.0, 10.0, 14.0],
304 [(13.0, 12.0), (12.0, 11.0), (11.0, 10.0), (11.0, 14.0)],
305 ),
306 ];
307 for (open, closes, expected) in rows {
308 let t0 = Utc::now();
309 let bars: Vec<Bar> = closes
310 .iter()
311 .enumerate()
312 .map(|(i, &c)| {
313 let o = if i == 0 { open } else { c };
314 Bar {
315 time: t0,
316 open: o,
317 high: o.max(c),
318 low: o.min(c),
319 close: c,
320 volume: 0.0,
321 colors: None,
322 payload: None,
323 }
324 })
325 .collect();
326 let lines = to_line_break_lines(&bars, &LineBreakConfig::default());
327 let got: Vec<(f64, f64)> = lines.iter().map(|l| (l.open, l.close)).collect();
328 assert_eq!(got, expected);
329 }
330 }
331}