kestrel_chartkit/indicator/
liquidity_fvg.rs1use std::collections::HashMap;
2
3use crate::indicator::{Indicator, IndicatorAlert, IndicatorOutput};
4use crate::model::Bar;
5
6pub 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 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 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 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 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 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}