use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct VolumeProfile {
period: usize,
bins: usize,
window: VecDeque<(f64, f64)>, }
impl VolumeProfile {
pub fn new(period: usize, bins: usize) -> Self {
Self {
period,
bins: bins.max(1),
window: VecDeque::with_capacity(period),
}
}
}
impl Next<(f64, f64)> for VolumeProfile {
type Output = f64;
fn next(&mut self, (price, volume): (f64, f64)) -> Self::Output {
self.window.push_back((price, volume));
if self.window.len() > self.period {
self.window.pop_front();
}
if self.window.is_empty() {
return f64::NAN;
}
let mut min_p = f64::MAX;
let mut max_p = f64::MIN;
for &(p, _) in self.window.iter() {
if p < min_p { min_p = p; }
if p > max_p { max_p = p; }
}
if min_p == max_p {
return min_p;
}
let mut histogram = vec![0.0; self.bins];
let bin_size = (max_p - min_p) / self.bins as f64;
for &(p, v) in self.window.iter() {
let mut bin_idx = ((p - min_p) / bin_size).floor() as usize;
if bin_idx >= self.bins {
bin_idx = self.bins - 1;
}
histogram[bin_idx] += v;
}
let mut max_v = -1.0;
let mut poc_idx = 0;
for (i, &v) in histogram.iter().enumerate() {
if v > max_v {
max_v = v;
poc_idx = i;
}
}
min_p + (poc_idx as f64 + 0.5) * bin_size
}
}
pub const VOLUME_PROFILE_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Volume Profile",
description: "Calculates the price level with the highest traded volume (Point of Control) over a sliding window.",
usage: "Use to identify significant support and resistance levels. The POC represents the price where most market activity occurred, often acting as a magnet for price or a strong barrier. Essential for volume spread analysis and auction market theory.",
keywords: &["volume", "profile", "poc", "support-resistance", "auction-market-theory"],
ehlers_summary: "Volume Profile is an advanced charting study that displays trading activity over a specified time period at specified price levels. The Point of Control (POC) is the single most important level in the profile, representing the price at which the most volume was traded. It serves as a key benchmark for identifying value areas and potential trend reversals.",
params: &[
ParamDef {
name: "period",
default: "200",
description: "Sliding window size",
},
ParamDef {
name: "bins",
default: "50",
description: "Number of price bins in the histogram",
},
],
formula_source: "https://www.tradingview.com/support/solutions/43000502040-volume-profile-visible-range-vpvr/",
formula_latex: r#"
\[
BinIdx = \lfloor \frac{Price - Price_{min}}{BinSize} \rfloor
\]
\[
POC = Price_{min} + (Idx_{max\_vol} + 0.5) \times BinSize
\]
"#,
gold_standard_file: "volume_profile.json",
category: "Volume",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::Next;
#[test]
fn test_volume_profile_basic() {
let mut vp = VolumeProfile::new(10, 5);
for _ in 0..5 {
vp.next((100.0, 10.0));
}
let res = vp.next((110.0, 5.0));
assert!(res >= 100.0 && res <= 105.0);
vp.next((110.0, 20.0));
vp.next((110.0, 20.0));
let res2 = vp.next((110.0, 20.0));
assert!(res2 >= 105.0); }
}