use super::bar::Bar;
use chrono::{DateTime, Utc};
#[derive(Debug, Clone)]
pub struct LineBreakConfig {
pub line_cnt: usize,
}
impl Default for LineBreakConfig {
fn default() -> Self {
Self { line_cnt: 3 }
}
}
impl LineBreakConfig {
pub fn new(line_cnt: usize) -> Self {
Self { line_cnt }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineDirection {
Up,
Down,
}
#[derive(Debug, Clone)]
pub struct LineBreakLine {
pub open: f64,
pub close: f64,
pub direction: LineDirection,
pub ts: DateTime<Utc>,
}
impl LineBreakLine {
pub fn high(&self) -> f64 {
self.open.max(self.close)
}
pub fn low(&self) -> f64 {
self.open.min(self.close)
}
pub fn is_bullish(&self) -> bool {
self.direction == LineDirection::Up
}
pub fn is_bearish(&self) -> bool {
self.direction == LineDirection::Down
}
}
pub fn to_line_break_lines(data: &[Bar], config: &LineBreakConfig) -> Vec<LineBreakLine> {
if data.is_empty() {
return Vec::new();
}
const MAX_LINES: usize = 10_000;
let mut lines: Vec<LineBreakLine> = Vec::new();
let first = &data[0];
let initial_direction = if first.close >= first.open {
LineDirection::Up
} else {
LineDirection::Down
};
lines.push(LineBreakLine {
open: first.open,
close: first.close,
direction: initial_direction,
ts: first.time,
});
for bar in data.iter().skip(1) {
if lines.is_empty() || lines.len() >= MAX_LINES {
continue;
}
let last_line = lines.last().unwrap();
let curr_direction = last_line.direction;
match curr_direction {
LineDirection::Up => {
if bar.close > last_line.high() {
lines.push(LineBreakLine {
open: last_line.close,
close: bar.close,
direction: LineDirection::Up,
ts: bar.time,
});
}
else {
let lookback = config.line_cnt.min(lines.len());
let reversal_low = lines[lines.len() - lookback..]
.iter()
.map(|l| l.low())
.fold(f64::INFINITY, f64::min);
if bar.close < reversal_low {
lines.push(LineBreakLine {
open: last_line.close,
close: bar.close,
direction: LineDirection::Down,
ts: bar.time,
});
}
}
}
LineDirection::Down => {
if bar.close < last_line.low() {
lines.push(LineBreakLine {
open: last_line.close,
close: bar.close,
direction: LineDirection::Down,
ts: bar.time,
});
}
else {
let lookback = config.line_cnt.min(lines.len());
let reversal_high = lines[lines.len() - lookback..]
.iter()
.map(|l| l.high())
.fold(f64::NEG_INFINITY, f64::max);
if bar.close > reversal_high {
lines.push(LineBreakLine {
open: last_line.close,
close: bar.close,
direction: LineDirection::Up,
ts: bar.time,
});
}
}
}
}
}
lines
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineBreakSignal {
Bullish,
Bearish,
BullishReversal,
BearishReversal,
None,
}
pub fn detect_signal(lines: &[LineBreakLine]) -> LineBreakSignal {
if lines.len() < 2 {
return LineBreakSignal::None;
}
let prev = &lines[lines.len() - 2];
let current = &lines[lines.len() - 1];
match (prev.direction, current.direction) {
(LineDirection::Down, LineDirection::Up) => LineBreakSignal::BullishReversal,
(LineDirection::Up, LineDirection::Down) => LineBreakSignal::BearishReversal,
(LineDirection::Up, LineDirection::Up) => LineBreakSignal::Bullish,
(LineDirection::Down, LineDirection::Down) => LineBreakSignal::Bearish,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_uptrend_bars() -> Vec<Bar> {
let now = Utc::now();
(0..10)
.map(|i| {
let base = 100.0 + (i as f64 * 2.0);
Bar {
time: now,
open: base,
high: base + 3.0,
low: base - 1.0,
close: base + 2.0,
volume: 1000.0,
}
})
.collect()
}
fn create_volatile_bars() -> Vec<Bar> {
let now = Utc::now();
let prices = [
(100.0, 102.0),
(102.0, 105.0),
(105.0, 103.0),
(103.0, 108.0),
(108.0, 100.0),
(100.0, 98.0),
(98.0, 105.0),
];
prices
.iter()
.map(|&(open, close)| Bar {
time: now,
open,
high: open.max(close) + 1.0,
low: open.min(close) - 1.0,
close,
volume: 1000.0,
})
.collect()
}
#[test]
fn test_config_creation() {
let config = LineBreakConfig::new(3);
assert_eq!(config.line_cnt, 3);
}
#[test]
fn test_uptrend_conversion() {
let bars = create_uptrend_bars();
let config = LineBreakConfig::new(3);
let lines = to_line_break_lines(&bars, &config);
assert!(!lines.is_empty());
for line in &lines {
assert_eq!(line.direction, LineDirection::Up);
}
}
#[test]
fn test_reversal_detection() {
let bars = create_volatile_bars();
let config = LineBreakConfig::new(3);
let lines = to_line_break_lines(&bars, &config);
let mut has_reversal = false;
for i in 1..lines.len() {
if lines[i - 1].direction != lines[i].direction {
has_reversal = true;
break;
}
}
assert!(has_reversal || lines.len() <= 1);
}
#[test]
fn test_signal_detection() {
let lines = vec![
LineBreakLine {
open: 100.0,
close: 105.0,
direction: LineDirection::Up,
ts: Utc::now(),
},
LineBreakLine {
open: 105.0,
close: 98.0,
direction: LineDirection::Down,
ts: Utc::now(),
},
];
let signal = detect_signal(&lines);
assert_eq!(signal, LineBreakSignal::BearishReversal);
}
}