Skip to main content

quantwave_core/indicators/
volume_profile.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use std::collections::VecDeque;
4
5/// Volume Profile (POC)
6///
7/// Tracks the volume distribution over a sliding window and returns the
8/// Point of Control (POC) - the price level with the highest volume.
9#[derive(Debug, Clone)]
10pub struct VolumeProfile {
11    period: usize,
12    bins: usize,
13    window: VecDeque<(f64, f64)>, // (price, volume)
14}
15
16impl VolumeProfile {
17    pub fn new(period: usize, bins: usize) -> Self {
18        Self {
19            period,
20            bins: bins.max(1),
21            window: VecDeque::with_capacity(period),
22        }
23    }
24}
25
26impl Next<(f64, f64)> for VolumeProfile {
27    type Output = f64;
28
29    fn next(&mut self, (price, volume): (f64, f64)) -> Self::Output {
30        self.window.push_back((price, volume));
31        if self.window.len() > self.period {
32            self.window.pop_front();
33        }
34
35        if self.window.is_empty() {
36            return f64::NAN;
37        }
38
39        // Find min and max price in the window
40        let mut min_p = f64::MAX;
41        let mut max_p = f64::MIN;
42        for &(p, _) in self.window.iter() {
43            if p < min_p { min_p = p; }
44            if p > max_p { max_p = p; }
45        }
46
47        if min_p == max_p {
48            return min_p;
49        }
50
51        // Create histogram
52        let mut histogram = vec![0.0; self.bins];
53        let bin_size = (max_p - min_p) / self.bins as f64;
54
55        for &(p, v) in self.window.iter() {
56            let mut bin_idx = ((p - min_p) / bin_size).floor() as usize;
57            if bin_idx >= self.bins {
58                bin_idx = self.bins - 1;
59            }
60            histogram[bin_idx] += v;
61        }
62
63        // Find bin with max volume
64        let mut max_v = -1.0;
65        let mut poc_idx = 0;
66        for (i, &v) in histogram.iter().enumerate() {
67            if v > max_v {
68                max_v = v;
69                poc_idx = i;
70            }
71        }
72
73        // Return the center of the POC bin
74        min_p + (poc_idx as f64 + 0.5) * bin_size
75    }
76}
77
78pub const VOLUME_PROFILE_METADATA: IndicatorMetadata = IndicatorMetadata {
79    name: "Volume Profile",
80    description: "Calculates the price level with the highest traded volume (Point of Control) over a sliding window.",
81    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.",
82    keywords: &["volume", "profile", "poc", "support-resistance", "auction-market-theory"],
83    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.",
84    params: &[
85        ParamDef {
86            name: "period",
87            default: "200",
88            description: "Sliding window size",
89        },
90        ParamDef {
91            name: "bins",
92            default: "50",
93            description: "Number of price bins in the histogram",
94        },
95    ],
96    formula_source: "https://www.tradingview.com/support/solutions/43000502040-volume-profile-visible-range-vpvr/",
97    formula_latex: r#"
98\[
99BinIdx = \lfloor \frac{Price - Price_{min}}{BinSize} \rfloor
100\]
101\[
102POC = Price_{min} + (Idx_{max\_vol} + 0.5) \times BinSize
103\]
104"#,
105    gold_standard_file: "volume_profile.json",
106    category: "Volume",
107};
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use crate::traits::Next;
113
114    #[test]
115    fn test_volume_profile_basic() {
116        let mut vp = VolumeProfile::new(10, 5);
117        // All volume at 100
118        for _ in 0..5 {
119            vp.next((100.0, 10.0));
120        }
121        // Some volume at 110
122        let res = vp.next((110.0, 5.0));
123        assert!(res >= 100.0 && res <= 105.0); // POC should still be in the 100 bin
124        
125        // More volume at 110
126        vp.next((110.0, 20.0));
127        vp.next((110.0, 20.0));
128        let res2 = vp.next((110.0, 20.0));
129        assert!(res2 >= 105.0); // POC should shift to higher bin
130    }
131}