indicators/signal/
liquidity.rs1use std::collections::{HashMap, VecDeque};
7
8use crate::error::IndicatorError;
9use crate::indicator::{Indicator, IndicatorOutput};
10use crate::registry::param_usize;
11use crate::types::Candle;
12
13#[derive(Debug, Clone)]
16pub struct LiquidityParams {
17 pub period: usize,
19 pub n_bins: usize,
21}
22
23impl Default for LiquidityParams {
24 fn default() -> Self {
25 Self {
26 period: 50,
27 n_bins: 20,
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
39pub struct LiquidityIndicator {
40 pub params: LiquidityParams,
41}
42
43impl LiquidityIndicator {
44 pub fn new(params: LiquidityParams) -> Self {
45 Self { params }
46 }
47 pub fn with_defaults() -> Self {
48 Self::new(LiquidityParams::default())
49 }
50}
51
52impl Indicator for LiquidityIndicator {
53 fn name(&self) -> &'static str {
54 "Liquidity"
55 }
56 fn required_len(&self) -> usize {
57 self.params.period
58 }
59 fn required_columns(&self) -> &[&'static str] {
60 &["high", "low", "close", "volume"]
61 }
62
63 fn calculate(&self, candles: &[Candle]) -> Result<IndicatorOutput, IndicatorError> {
64 self.check_len(candles)?;
65 let p = &self.params;
66 let mut liq = LiquidityProfile::new(p.period, p.n_bins);
67 let n = candles.len();
68 let mut poc = vec![f64::NAN; n];
69 let mut buy_pct = vec![f64::NAN; n];
70 let mut imbalance = vec![f64::NAN; n];
71 let mut vah = vec![f64::NAN; n];
72 let mut val = vec![f64::NAN; n];
73 for (i, c) in candles.iter().enumerate() {
74 liq.update(c);
75 poc[i] = liq.poc_price.unwrap_or(f64::NAN);
76 buy_pct[i] = liq.buy_pct;
77 imbalance[i] = liq.imbalance;
78 vah[i] = liq.vah.unwrap_or(f64::NAN);
79 val[i] = liq.val.unwrap_or(f64::NAN);
80 }
81 Ok(IndicatorOutput::from_pairs([
82 ("liq_poc", poc),
83 ("liq_buy_pct", buy_pct),
84 ("liq_imbalance", imbalance),
85 ("liq_vah", vah),
86 ("liq_val", val),
87 ]))
88 }
89}
90
91pub fn factory<S: ::std::hash::BuildHasher>(
94 params: &HashMap<String, String, S>,
95) -> Result<Box<dyn Indicator>, IndicatorError> {
96 let period = param_usize(params, "period", 50)?;
97 let n_bins = param_usize(params, "n_bins", 20)?;
98 Ok(Box::new(LiquidityIndicator::new(LiquidityParams {
99 period,
100 n_bins,
101 })))
102}
103
104#[derive(Debug)]
106pub struct LiquidityProfile {
107 period: usize,
108 n_bins: usize,
109 candles: VecDeque<Candle>,
110
111 pub poc_price: Option<f64>,
112 pub vah: Option<f64>,
113 pub val: Option<f64>,
114 pub buy_liq: f64,
115 pub sell_liq: f64,
116 pub imbalance: f64,
117 pub buy_pct: f64,
118}
119
120impl LiquidityProfile {
121 pub fn new(period: usize, n_bins: usize) -> Self {
122 Self {
123 period,
124 n_bins,
125 candles: VecDeque::with_capacity(period),
126 poc_price: None,
127 vah: None,
128 val: None,
129 buy_liq: 0.0,
130 sell_liq: 0.0,
131 imbalance: 0.0,
132 buy_pct: 0.5,
133 }
134 }
135
136 pub fn update(&mut self, candle: &Candle) {
137 if self.candles.len() == self.period {
138 self.candles.pop_front();
139 }
140 self.candles.push_back(candle.clone());
141
142 if self.candles.len() < 5 {
143 return;
144 }
145
146 let h: f64 = self
147 .candles
148 .iter()
149 .map(|c| c.high)
150 .fold(f64::NEG_INFINITY, f64::max);
151 let l: f64 = self
152 .candles
153 .iter()
154 .map(|c| c.low)
155 .fold(f64::INFINITY, f64::min);
156 let rng = h - l;
157 if rng <= 0.0 {
158 return;
159 }
160
161 let step = rng / self.n_bins as f64;
162 let mut bins = vec![0.0_f64; self.n_bins];
163
164 for c in &self.candles {
165 let bar_rng = c.high - c.low;
166 if bar_rng <= 0.0 || c.volume <= 0.0 {
167 continue;
168 }
169 #[allow(clippy::needless_range_loop)]
170 for i in 0..self.n_bins {
171 let bin_lo = l + step * i as f64;
172 let bin_hi = bin_lo + step;
173 let overlap = c.high.min(bin_hi) - c.low.max(bin_lo);
174 if overlap > 0.0 {
175 bins[i] += c.volume * overlap / bar_rng;
176 }
177 }
178 }
179
180 let poc_idx = bins
182 .iter()
183 .enumerate()
184 .max_by(|a, b| a.1.total_cmp(b.1))
185 .map_or(0, |(i, _)| i);
186 self.poc_price = Some(l + step * poc_idx as f64 + step / 2.0);
187
188 let total_vol: f64 = bins.iter().sum();
190 let target = total_vol * 0.70;
191 let mut area_vol = bins[poc_idx];
192 let mut upper = poc_idx;
193 let mut lower = poc_idx;
194
195 while area_vol < target {
196 let can_up = upper + 1 < self.n_bins;
197 let can_down = lower > 0;
198 if !can_up && !can_down {
199 break;
200 }
201 let vol_up = if can_up { bins[upper + 1] } else { -1.0 };
202 let vol_down = if can_down { bins[lower - 1] } else { -1.0 };
203 if vol_up >= vol_down {
204 upper += 1;
205 area_vol += bins[upper];
206 } else {
207 lower -= 1;
208 area_vol += bins[lower];
209 }
210 }
211
212 self.vah = Some(l + step * upper as f64 + step / 2.0);
213 self.val = Some(l + step * lower as f64 + step / 2.0);
214
215 let cl = candle.close;
217 self.buy_liq = (0..self.n_bins)
218 .map(|i| {
219 if l + step * i as f64 + step / 2.0 < cl {
220 bins[i]
221 } else {
222 0.0
223 }
224 })
225 .sum();
226 self.sell_liq = (0..self.n_bins)
227 .map(|i| {
228 if l + step * i as f64 + step / 2.0 >= cl {
229 bins[i]
230 } else {
231 0.0
232 }
233 })
234 .sum();
235
236 let total = self.buy_liq + self.sell_liq;
237 self.buy_pct = if total > 0.0 {
238 self.buy_liq / total
239 } else {
240 0.5
241 };
242 self.imbalance = self.buy_liq - self.sell_liq;
243 }
244
245 pub fn bullish(&self) -> bool {
246 self.imbalance > 0.0
247 }
248}