Skip to main content

quantwave_core/indicators/
volume_profile.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3use crate::utils::RingBuffer as 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 {
44                min_p = p;
45            }
46            if p > max_p {
47                max_p = p;
48            }
49        }
50
51        if min_p == max_p {
52            return min_p;
53        }
54
55        // Create histogram
56        let mut histogram = vec![0.0; self.bins];
57        let bin_size = (max_p - min_p) / self.bins as f64;
58
59        for &(p, v) in self.window.iter() {
60            let mut bin_idx = ((p - min_p) / bin_size).floor() as usize;
61            if bin_idx >= self.bins {
62                bin_idx = self.bins - 1;
63            }
64            histogram[bin_idx] += v;
65        }
66
67        // Find bin with max volume
68        let mut max_v = -1.0;
69        let mut poc_idx = 0;
70        for (i, &v) in histogram.iter().enumerate() {
71            if v > max_v {
72                max_v = v;
73                poc_idx = i;
74            }
75        }
76
77        // Return the center of the POC bin
78        min_p + (poc_idx as f64 + 0.5) * bin_size
79    }
80}
81
82pub const VOLUME_PROFILE_METADATA: IndicatorMetadata = IndicatorMetadata {
83    name: "Volume Profile",
84    description: "Calculates the price level with the highest traded volume (Point of Control) over a sliding window.",
85    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.",
86    keywords: &[
87        "volume",
88        "profile",
89        "poc",
90        "support-resistance",
91        "auction-market-theory",
92    ],
93    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.",
94    params: &[
95        ParamDef {
96            name: "period",
97            default: "200",
98            description: "Sliding window size",
99        },
100        ParamDef {
101            name: "bins",
102            default: "50",
103            description: "Number of price bins in the histogram",
104        },
105    ],
106    formula_source: "https://www.tradingview.com/support/solutions/43000502040-volume-profile-visible-range-vpvr/",
107    formula_latex: r#"
108\[
109BinIdx = \lfloor \frac{Price - Price_{min}}{BinSize} \rfloor
110\]
111\[
112POC = Price_{min} + (Idx_{max\_vol} + 0.5) \times BinSize
113\]
114"#,
115    gold_standard_file: "volume_profile.json",
116    category: "Volume",
117};
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::traits::Next;
123
124    #[test]
125    fn test_volume_profile_basic() {
126        let mut vp = VolumeProfile::new(10, 5);
127        // All volume at 100
128        for _ in 0..5 {
129            vp.next((100.0, 10.0));
130        }
131        // Some volume at 110
132        let res = vp.next((110.0, 5.0));
133        assert!(res >= 100.0 && res <= 105.0); // POC should still be in the 100 bin
134
135        // More volume at 110
136        vp.next((110.0, 20.0));
137        vp.next((110.0, 20.0));
138        let res2 = vp.next((110.0, 20.0));
139        assert!(res2 >= 105.0); // POC should shift to higher bin
140    }
141}