use crate::bar_indicators::indicator_value::IndicatorValue;
use crate::bar_indicators::tick_consumer::TickConsumer;
use crate::types::Tick;
use std::collections::HashMap;
#[derive(Clone)]
pub struct FootprintImbalance {
price_bucket: f64,
threshold_pct: f64,
levels: HashMap<i64, (f64, f64)>,
last_signal: i8,
last_imb_price: f64,
last_imb_pct: f64,
}
impl FootprintImbalance {
pub fn new(price_bucket: f64, threshold_pct: f64) -> Self {
Self {
price_bucket: price_bucket.max(1e-9),
threshold_pct: threshold_pct.clamp(0.0, 100.0),
levels: HashMap::new(),
last_signal: 0,
last_imb_price: 0.0,
last_imb_pct: 0.0,
}
}
pub fn update_tick(&mut self, tick: &Tick) -> IndicatorValue {
let bucket = (tick.price / self.price_bucket).floor() as i64;
let entry = self.levels.entry(bucket).or_insert((0.0, 0.0));
if tick.is_buy {
entry.0 += tick.size;
} else {
entry.1 += tick.size;
}
let mut max_signed_pct = 0.0f64;
let mut max_price = 0.0f64;
for (&bkt, &(buy, sell)) in &self.levels {
let total = buy + sell;
if total <= 0.0 {
continue;
}
let signed_pct = ((buy - sell) / total) * 100.0;
if signed_pct.abs() > max_signed_pct.abs() {
max_signed_pct = signed_pct;
max_price = bkt as f64 * self.price_bucket;
}
}
self.last_signal = if max_signed_pct >= self.threshold_pct {
1
} else if max_signed_pct <= -self.threshold_pct {
-1
} else {
0
};
self.last_imb_price = max_price;
self.last_imb_pct = max_signed_pct.abs();
self.value()
}
pub fn close_bar(&mut self) {
let mut max_signed_pct = 0.0f64;
let mut max_price = 0.0f64;
for (&bucket, &(buy, sell)) in &self.levels {
let total = buy + sell;
if total <= 0.0 {
continue;
}
let signed_pct = ((buy - sell) / total) * 100.0; if signed_pct.abs() > max_signed_pct.abs() {
max_signed_pct = signed_pct;
max_price = bucket as f64 * self.price_bucket;
}
}
self.last_signal = if max_signed_pct >= self.threshold_pct {
1
} else if max_signed_pct <= -self.threshold_pct {
-1
} else {
0
};
self.last_imb_price = max_price;
self.last_imb_pct = max_signed_pct.abs();
self.levels.clear();
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Triple(
self.last_signal as f64,
self.last_imb_price,
self.last_imb_pct,
)
}
pub fn reset(&mut self) {
self.levels.clear();
self.last_signal = 0;
self.last_imb_price = 0.0;
self.last_imb_pct = 0.0;
}
pub fn is_ready(&self) -> bool {
!self.levels.is_empty() || self.last_imb_pct > 0.0
}
pub fn signal(&self) -> i8 { self.last_signal }
pub fn imbalance_price(&self) -> f64 { self.last_imb_price }
pub fn imbalance_pct(&self) -> f64 { self.last_imb_pct }
}
impl TickConsumer for FootprintImbalance {
fn update_tick(&mut self, tick: &Tick) -> IndicatorValue {
FootprintImbalance::update_tick(self, tick)
}
fn value(&self) -> IndicatorValue { FootprintImbalance::value(self) }
fn reset(&mut self) { FootprintImbalance::reset(self) }
fn is_ready(&self) -> bool { FootprintImbalance::is_ready(self) }
}
#[cfg(test)]
mod tests {
use super::*;
fn buy_tick(price: f64, qty: f64) -> Tick {
Tick::new(0, price, qty, true)
}
fn sell_tick(price: f64, qty: f64) -> Tick {
Tick::new(0, price, qty, false)
}
#[test]
fn test_buy_extreme_signals_plus_one() {
let mut fi = FootprintImbalance::new(1.0, 75.0);
for _ in 0..10 {
fi.update_tick(&buy_tick(100.0, 1.0));
}
fi.close_bar();
assert_eq!(fi.signal(), 1);
assert!((fi.imbalance_pct() - 100.0).abs() < 1e-9);
}
#[test]
fn test_sell_extreme_signals_minus_one() {
let mut fi = FootprintImbalance::new(1.0, 75.0);
for _ in 0..10 {
fi.update_tick(&sell_tick(100.0, 1.0));
}
fi.close_bar();
assert_eq!(fi.signal(), -1);
}
#[test]
fn test_balanced_no_signal() {
let mut fi = FootprintImbalance::new(1.0, 75.0);
for _ in 0..5 {
fi.update_tick(&buy_tick(100.0, 1.0));
fi.update_tick(&sell_tick(100.0, 1.0));
}
fi.close_bar();
assert_eq!(fi.signal(), 0);
}
#[test]
fn test_reset_clears_state() {
let mut fi = FootprintImbalance::new(1.0, 75.0);
fi.update_tick(&buy_tick(100.0, 10.0));
fi.close_bar();
fi.reset();
assert_eq!(fi.signal(), 0);
assert_eq!(fi.imbalance_pct(), 0.0);
assert!(!fi.is_ready());
}
}