Skip to main content

kestrel_chartkit/indicator/
liquidity_fvg.rs

1use std::collections::HashMap;
2
3use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
4use crate::model::Bar;
5
6/// Smart Money Fair Value Gap (FVG) & Liquidity Sweep Engine.
7/// Detects institutional price imbalances and liquidity pool sweeps.
8pub struct LiquidityFvgEngine {
9    lookback: usize,
10    window: Vec<Bar>,
11    alerts: Vec<IndicatorAlert>,
12}
13
14impl LiquidityFvgEngine {
15    pub fn new(lookback: usize) -> Self {
16        Self {
17            lookback,
18            window: Vec::with_capacity(lookback + 5),
19            alerts: Vec::new(),
20        }
21    }
22}
23
24impl Indicator for LiquidityFvgEngine {
25    fn name(&self) -> &str {
26        "liquidity_fvg"
27    }
28
29    fn warmup_period(&self) -> usize {
30        self.lookback.max(3)
31    }
32
33    fn reset(&mut self) {
34        self.window.clear();
35        self.alerts.clear();
36    }
37
38    fn on_bar(&mut self, bar: &Bar) -> Option<IndicatorOutput> {
39        self.window.push(bar.clone());
40        if self.window.len() > self.lookback + 5 {
41            self.window.remove(0);
42        }
43
44        self.alerts.clear();
45
46        if self.window.len() < 3 {
47            return Some(IndicatorOutput::new(0.0));
48        }
49
50        let n = self.window.len();
51        let curr = &self.window[n - 1];
52        let prev2 = &self.window[n - 3];
53
54        let mut fvg_type = 0.0f64;
55        let mut gap_size = 0.0f64;
56
57        // 1. Bullish Fair Value Gap (Low[t] > High[t-2])
58        if curr.low > prev2.high {
59            fvg_type = 1.0;
60            gap_size = curr.low - prev2.high;
61            self.alerts.push(IndicatorAlert::new(
62                "bullish_fvg",
63                format!(
64                    "Bullish Fair Value Gap (FVG Zone ${:.2} - ${:.2})",
65                    prev2.high, curr.low
66                ),
67                0.85,
68            ));
69        }
70        // 2. Bearish Fair Value Gap (High[t] < Low[t-2])
71        else if curr.high < prev2.low {
72            fvg_type = -1.0;
73            gap_size = prev2.low - curr.high;
74            self.alerts.push(IndicatorAlert::new(
75                "bearish_fvg",
76                format!(
77                    "Bearish Fair Value Gap (FVG Zone ${:.2} - ${:.2})",
78                    curr.high, prev2.low
79                ),
80                0.85,
81            ));
82        }
83
84        // 3. Liquidity Sweep Detection over lookback window
85        if n > self.lookback {
86            let prev_bars = &self.window[n - 1 - self.lookback..n - 1];
87            let recent_highest = prev_bars
88                .iter()
89                .map(|b| b.high)
90                .fold(f64::NEG_INFINITY, f64::max);
91            let recent_lowest = prev_bars
92                .iter()
93                .map(|b| b.low)
94                .fold(f64::INFINITY, f64::min);
95
96            // Bullish Liquidity Sweep (Low pierced recent lowest, but Close > recent lowest)
97            if curr.low < recent_lowest && curr.close > recent_lowest {
98                self.alerts.push(IndicatorAlert::new(
99                    "bullish_liquidity_sweep",
100                    format!(
101                        "Bullish Liquidity Sweep (Pierced ${:.2} Support, Reclaimed Close)",
102                        recent_lowest
103                    ),
104                    0.95,
105                ));
106            }
107            // Bearish Liquidity Sweep (High pierced recent highest, but Close < recent highest)
108            else if curr.high > recent_highest && curr.close < recent_highest {
109                self.alerts.push(IndicatorAlert::new(
110                    "bearish_liquidity_sweep",
111                    format!(
112                        "Bearish Liquidity Sweep (Pierced ${:.2} Resistance, Reclaimed Close)",
113                        recent_highest
114                    ),
115                    0.95,
116                ));
117            }
118        }
119
120        let mut extra = HashMap::new();
121        extra.insert("fvg_type".to_string(), fvg_type);
122        extra.insert("gap_size".to_string(), gap_size);
123
124        Some(IndicatorOutput::with_extra(fvg_type, extra))
125    }
126
127    fn alerts(&self) -> Vec<IndicatorAlert> {
128        self.alerts.clone()
129    }
130}
131
132pub fn build_liquidity_fvg(params: &HashMap<String, f64>) -> LiquidityFvgEngine {
133    let lookback = params.get("lookback").copied().unwrap_or(20.0) as usize;
134    LiquidityFvgEngine::new(lookback)
135}