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 FootprintChart {
price_bucket: f64,
levels: HashMap<i64, (f64, f64)>,
total_buy: f64,
total_sell: f64,
last_poc_price: f64,
last_max_imbalance_pct: f64,
last_max_imbalance_price: f64,
last_net_delta: f64,
last_total_volume: f64,
}
impl FootprintChart {
pub fn new(price_bucket: f64) -> Self {
Self {
price_bucket: price_bucket.max(1e-9),
levels: HashMap::new(),
total_buy: 0.0,
total_sell: 0.0,
last_poc_price: 0.0,
last_max_imbalance_pct: 0.0,
last_max_imbalance_price: 0.0,
last_net_delta: 0.0,
last_total_volume: 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;
self.total_buy += tick.size;
} else {
entry.1 += tick.size;
self.total_sell += tick.size;
}
self.last_total_volume = self.total_buy + self.total_sell;
self.last_net_delta = self.total_buy - self.total_sell;
IndicatorValue::Single(self.last_net_delta)
}
pub fn update_bar(&mut self, _o: f64, h: f64, l: f64, c: f64, v: f64) -> IndicatorValue {
let mid = (h + l) / 2.0;
let buy_frac = if h > l { (c - l) / (h - l) } else { 0.5 };
let buy_vol = v * buy_frac;
let sell_vol = v * (1.0 - buy_frac);
let _ = mid;
let bucket = (c / self.price_bucket).floor() as i64;
let entry = self.levels.entry(bucket).or_insert((0.0, 0.0));
entry.0 += buy_vol;
entry.1 += sell_vol;
self.total_buy += buy_vol;
self.total_sell += sell_vol;
IndicatorValue::Single(self.total_buy - self.total_sell)
}
pub fn close_bar(&mut self) {
if self.levels.is_empty() {
return;
}
let mut poc_bucket = 0i64;
let mut poc_vol = 0.0f64;
let mut max_imb_pct = 0.0f64;
let mut max_imb_bucket = 0i64;
for (&bucket, &(buy, sell)) in &self.levels {
let total = buy + sell;
if total > poc_vol {
poc_vol = total;
poc_bucket = bucket;
}
if total > 0.0 {
let imb_pct = ((buy - sell).abs() / total) * 100.0;
if imb_pct > max_imb_pct {
max_imb_pct = imb_pct;
max_imb_bucket = bucket;
}
}
}
self.last_poc_price = poc_bucket as f64 * self.price_bucket;
self.last_max_imbalance_pct = max_imb_pct;
self.last_max_imbalance_price = max_imb_bucket as f64 * self.price_bucket;
self.last_net_delta = self.total_buy - self.total_sell;
self.last_total_volume = self.total_buy + self.total_sell;
self.levels.clear();
self.total_buy = 0.0;
self.total_sell = 0.0;
}
pub fn poc_price(&self) -> f64 { self.last_poc_price }
pub fn max_imbalance_pct(&self) -> f64 { self.last_max_imbalance_pct }
pub fn max_imbalance_price(&self) -> f64 { self.last_max_imbalance_price }
pub fn net_delta(&self) -> f64 { self.last_net_delta }
pub fn total_volume(&self) -> f64 { self.last_total_volume }
pub fn current_levels(&self) -> &HashMap<i64, (f64, f64)> { &self.levels }
pub fn total_buy(&self) -> f64 { self.total_buy }
pub fn total_sell(&self) -> f64 { self.total_sell }
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Triple(self.last_net_delta, self.last_poc_price, self.last_total_volume)
}
pub fn is_ready(&self) -> bool {
self.last_total_volume > 0.0
}
pub fn reset(&mut self) {
self.levels.clear();
self.total_buy = 0.0;
self.total_sell = 0.0;
self.last_poc_price = 0.0;
self.last_max_imbalance_pct = 0.0;
self.last_max_imbalance_price = 0.0;
self.last_net_delta = 0.0;
self.last_total_volume = 0.0;
}
}
impl TickConsumer for FootprintChart {
fn update_tick(&mut self, tick: &Tick) -> IndicatorValue {
FootprintChart::update_tick(self, tick)
}
fn value(&self) -> IndicatorValue { FootprintChart::value(self) }
fn reset(&mut self) { FootprintChart::reset(self) }
fn is_ready(&self) -> bool { FootprintChart::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_footprint_creation() {
let fp = FootprintChart::new(0.01);
assert!(!fp.is_ready());
assert_eq!(fp.net_delta(), 0.0);
}
#[test]
fn test_accumulate_and_close_bar() {
let mut fp = FootprintChart::new(1.0);
for _ in 0..5 {
fp.update_tick(&buy_tick(100.0, 10.0));
}
for _ in 0..3 {
fp.update_tick(&sell_tick(101.0, 5.0));
}
fp.close_bar();
assert!((fp.net_delta() - 35.0).abs() < 1e-9, "net delta should be 50-15=35");
assert_eq!(fp.poc_price(), 100.0, "POC should be at price 100 (50 vol > 15 vol)");
assert!((fp.total_volume() - 65.0).abs() < 1e-9);
}
#[test]
fn test_max_imbalance_is_100_pct_for_pure_buy() {
let mut fp = FootprintChart::new(1.0);
fp.update_tick(&buy_tick(100.0, 20.0));
fp.close_bar();
assert!((fp.max_imbalance_pct() - 100.0).abs() < 1e-9);
assert_eq!(fp.max_imbalance_price(), 100.0);
}
#[test]
fn test_footprint_bar_synthetic() {
let mut fp = FootprintChart::new(1.0);
fp.update_bar(100.0, 110.0, 90.0, 108.0, 400.0);
assert!(fp.total_buy() > fp.total_sell());
}
#[test]
fn test_footprint_reset() {
let mut fp = FootprintChart::new(1.0);
fp.update_tick(&buy_tick(100.0, 10.0));
fp.close_bar();
fp.reset();
assert!(!fp.is_ready());
assert_eq!(fp.net_delta(), 0.0);
}
#[test]
fn test_close_bar_clears_in_progress() {
let mut fp = FootprintChart::new(1.0);
fp.update_tick(&buy_tick(100.0, 10.0));
assert!(!fp.current_levels().is_empty());
fp.close_bar();
assert!(fp.current_levels().is_empty());
assert!(fp.is_ready()); }
}