use super::{Indicator, IndicatorAlert, IndicatorOutput};
use crate::model::Bar;
use std::collections::{HashMap, VecDeque};
#[derive(Debug, Clone)]
pub struct LiquiditySweepEngine {
pivot_len: usize,
tolerance_pct: f64,
bars: VecDeque<Bar>,
pivot_highs: Vec<f64>,
pivot_lows: Vec<f64>,
sweep_detected: i8, }
impl LiquiditySweepEngine {
pub fn new(pivot_len: usize, tolerance_pct: f64) -> Self {
Self {
pivot_len: pivot_len.max(2),
tolerance_pct: tolerance_pct.max(0.01),
bars: VecDeque::with_capacity(pivot_len * 2 + 1),
pivot_highs: Vec::new(),
pivot_lows: Vec::new(),
sweep_detected: 0,
}
}
pub fn with_defaults() -> Self {
Self::new(5, 0.2) }
}
impl Indicator for LiquiditySweepEngine {
fn name(&self) -> &str {
"liquidity_sweeps"
}
fn warmup_period(&self) -> usize {
self.pivot_len * 2 + 1
}
fn reset(&mut self) {
self.bars.clear();
self.pivot_highs.clear();
self.pivot_lows.clear();
self.sweep_detected = 0;
}
fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
self.bars.push_back(bar.clone());
if self.bars.len() > self.pivot_len * 2 + 1 {
self.bars.pop_front();
}
if self.bars.len() < self.pivot_len * 2 + 1 {
return None;
}
let mid_idx = self.pivot_len;
let mid_bar = &self.bars[mid_idx];
let is_pivot_high = self
.bars
.iter()
.enumerate()
.all(|(i, b)| i == mid_idx || b.high <= mid_bar.high);
let is_pivot_low = self
.bars
.iter()
.enumerate()
.all(|(i, b)| i == mid_idx || b.low >= mid_bar.low);
if is_pivot_high {
self.pivot_highs.push(mid_bar.high);
if self.pivot_highs.len() > 20 {
self.pivot_highs.remove(0);
}
}
if is_pivot_low {
self.pivot_lows.push(mid_bar.low);
if self.pivot_lows.len() > 20 {
self.pivot_lows.remove(0);
}
}
self.sweep_detected = 0;
for &ph in &self.pivot_highs {
if bar.high > ph && bar.close < ph {
self.sweep_detected = -1;
break;
}
}
if self.sweep_detected == 0 {
for &pl in &self.pivot_lows {
if bar.low < pl && bar.close > pl {
self.sweep_detected = 1;
break;
}
}
}
let eqh_count = self
.pivot_highs
.windows(2)
.filter(|w| (w[0] - w[1]).abs() / w[0] * 100.0 <= self.tolerance_pct)
.count();
let eql_count = self
.pivot_lows
.windows(2)
.filter(|w| (w[0] - w[1]).abs() / w[0] * 100.0 <= self.tolerance_pct)
.count();
let mut extra = HashMap::new();
extra.insert("sweep".to_string(), self.sweep_detected as f64);
extra.insert("eqh_count".to_string(), eqh_count as f64);
extra.insert("eql_count".to_string(), eql_count as f64);
Some(IndicatorOutput::with_extra(
self.sweep_detected as f64,
extra,
))
}
fn alerts(&self) -> Vec<IndicatorAlert> {
let mut alerts = Vec::new();
if self.sweep_detected == 1 {
alerts.push(IndicatorAlert::new(
"sweep",
"Bullish Liquidity Sweep & Reclaim",
0.85,
));
} else if self.sweep_detected == -1 {
alerts.push(IndicatorAlert::new(
"sweep",
"Bearish Liquidity Sweep & Reclaim",
0.85,
));
}
alerts
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_liquidity_sweep_detection() {
let mut sweep = LiquiditySweepEngine::with_defaults();
for i in 0..20 {
let b = Bar::new(i, 100.0, 105.0, 95.0, 100.0, 1000.0);
sweep.on_bar(&b);
}
let sweep_bar = Bar::new(20, 99.0, 101.0, 90.0, 98.0, 1000.0);
let out = sweep.on_bar(&sweep_bar);
assert!(out.is_some());
}
}