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>(params: &HashMap<String, String, S>) -> Result<Box<dyn Indicator>, IndicatorError> {
94 let period = param_usize(params, "period", 50)?;
95 let n_bins = param_usize(params, "n_bins", 20)?;
96 Ok(Box::new(LiquidityIndicator::new(LiquidityParams {
97 period,
98 n_bins,
99 })))
100}
101
102#[derive(Debug)]
104pub struct LiquidityProfile {
105 period: usize,
106 n_bins: usize,
107 candles: VecDeque<Candle>,
108
109 pub poc_price: Option<f64>,
110 pub vah: Option<f64>,
111 pub val: Option<f64>,
112 pub buy_liq: f64,
113 pub sell_liq: f64,
114 pub imbalance: f64,
115 pub buy_pct: f64,
116}
117
118impl LiquidityProfile {
119 pub fn new(period: usize, n_bins: usize) -> Self {
120 Self {
121 period,
122 n_bins,
123 candles: VecDeque::with_capacity(period),
124 poc_price: None,
125 vah: None,
126 val: None,
127 buy_liq: 0.0,
128 sell_liq: 0.0,
129 imbalance: 0.0,
130 buy_pct: 0.5,
131 }
132 }
133
134 pub fn update(&mut self, candle: &Candle) {
135 if self.candles.len() == self.period {
136 self.candles.pop_front();
137 }
138 self.candles.push_back(candle.clone());
139
140 if self.candles.len() < 5 {
141 return;
142 }
143
144 let h: f64 = self
145 .candles
146 .iter()
147 .map(|c| c.high)
148 .fold(f64::NEG_INFINITY, f64::max);
149 let l: f64 = self
150 .candles
151 .iter()
152 .map(|c| c.low)
153 .fold(f64::INFINITY, f64::min);
154 let rng = h - l;
155 if rng <= 0.0 {
156 return;
157 }
158
159 let step = rng / self.n_bins as f64;
160 let mut bins = vec![0.0_f64; self.n_bins];
161
162 for c in &self.candles {
163 let bar_rng = c.high - c.low;
164 if bar_rng <= 0.0 || c.volume <= 0.0 {
165 continue;
166 }
167 #[allow(clippy::needless_range_loop)]
168 for i in 0..self.n_bins {
169 let bin_lo = l + step * i as f64;
170 let bin_hi = bin_lo + step;
171 let overlap = c.high.min(bin_hi) - c.low.max(bin_lo);
172 if overlap > 0.0 {
173 bins[i] += c.volume * overlap / bar_rng;
174 }
175 }
176 }
177
178 let poc_idx = bins
180 .iter()
181 .enumerate()
182 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
183 .map_or(0, |(i, _)| i);
184 self.poc_price = Some(l + step * poc_idx as f64 + step / 2.0);
185
186 let total_vol: f64 = bins.iter().sum();
188 let target = total_vol * 0.70;
189 let mut area_vol = bins[poc_idx];
190 let mut upper = poc_idx;
191 let mut lower = poc_idx;
192
193 while area_vol < target {
194 let can_up = upper + 1 < self.n_bins;
195 let can_down = lower > 0;
196 if !can_up && !can_down {
197 break;
198 }
199 let vol_up = if can_up { bins[upper + 1] } else { -1.0 };
200 let vol_down = if can_down { bins[lower - 1] } else { -1.0 };
201 if vol_up >= vol_down {
202 upper += 1;
203 area_vol += bins[upper];
204 } else {
205 lower -= 1;
206 area_vol += bins[lower];
207 }
208 }
209
210 self.vah = Some(l + step * upper as f64 + step / 2.0);
211 self.val = Some(l + step * lower as f64 + step / 2.0);
212
213 let cl = candle.close;
215 self.buy_liq = (0..self.n_bins)
216 .map(|i| {
217 if l + step * i as f64 + step / 2.0 < cl {
218 bins[i]
219 } else {
220 0.0
221 }
222 })
223 .sum();
224 self.sell_liq = (0..self.n_bins)
225 .map(|i| {
226 if l + step * i as f64 + step / 2.0 >= cl {
227 bins[i]
228 } else {
229 0.0
230 }
231 })
232 .sum();
233
234 let total = self.buy_liq + self.sell_liq;
235 self.buy_pct = if total > 0.0 {
236 self.buy_liq / total
237 } else {
238 0.5
239 };
240 self.imbalance = self.buy_liq - self.sell_liq;
241 }
242
243 pub fn bullish(&self) -> bool {
244 self.imbalance > 0.0
245 }
246}