Skip to main content

finance_solution/stocks/ta/
vwap.rs

1//! # VWAP (volume-weighted average price)
2//!
3//! ```text
4//! typical_t = (H + L + C) / 3   // or close-only via VwapPriceSource
5//! vwap_t    = sum(typical_i * vol_i) / sum(vol_i)   // over session or rolling window
6//! ```
7//!
8//! ## Word problem
9//!
10//! > Session opens; bars print (TP=10, V=100) then (TP=11, V=100). What is cumulative VWAP?
11//!
12//! Expect: first bar 10; second `(10*100 + 11*100) / 200 = 10.5`.
13//!
14//! ```
15//! use finance_solution::stocks::ta::{vwap, VwapParams};
16//! let h = [10.0, 11.0];
17//! let l = [10.0, 11.0];
18//! let c = [10.0, 11.0];
19//! let v = [100.0, 100.0];
20//! let s = vwap(&h, &l, &c, &v, VwapParams::cumulative_typical()).unwrap();
21//! assert!((s.vwap[0].unwrap() - 10.0).abs() < 1e-12);
22//! assert!((s.vwap[1].unwrap() - 10.5).abs() < 1e-12);
23//! ```
24//!
25//! ## Modes ([`VwapMode`])
26//!
27//! - **Cumulative** — from bar 0 (or from last [`VwapState::reset`]) — classic intraday.
28//! - **Rolling** — last `period` bars only.
29//!
30//! **Day reset is your policy:** call `VwapState::reset()` at session open, or rebuild state
31//! from the day’s history. The library never invents a calendar.
32//!
33//! ## Quant pattern
34//!
35//! ```
36//! use finance_solution::stocks::ta::{VwapParams, ValidatedVwap, VwapState};
37//!
38//! const INTRADAY: VwapParams = VwapParams::cumulative_typical();
39//! let eng = ValidatedVwap::new(INTRADAY).unwrap();
40//! # let h = [10.0, 11.0, 12.0];
41//! # let l = [9.0, 10.0, 11.0];
42//! # let c = [9.5, 10.5, 11.5];
43//! # let vol = [100.0, 200.0, 150.0];
44//! let s = eng.compute(&h, &l, &c, &vol).unwrap();
45//! let mut live = VwapState::new(INTRADAY).unwrap();
46//! let _ = live.push_bars(&h, &l, &c, &vol).unwrap();
47//! // live.reset(); // e.g. regular-session open — you decide
48//! assert!(s.vwap[2].unwrap().is_finite());
49//! ```
50//!
51//! ## Sample solution table
52//!
53//! ```text
54//! period  typical  volume     vwap
55//! ------  -------  ------  -------
56//!      0   9.5000  100.00   9.5000
57//!      1  10.5000  200.00  10.1667
58//!      2  11.5000  150.00  10.6111
59//! ```
60
61use crate::stocks::ta::common::{
62    opt_cell, require_hlc, require_same_len, validate_positive_volume,
63};
64use crate::util::error::FinanceResult;
65use crate::util::primitives::PeriodLength;
66use crate::{columns_with_strings, print_table_locale_opt};
67
68/// Price input for VWAP numerator.
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
70pub enum VwapPriceSource {
71    /// `(high + low + close) / 3`.
72    #[default]
73    Typical,
74    /// Close only.
75    Close,
76}
77
78/// Cumulative session vs rolling window.
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
80pub enum VwapMode {
81    Cumulative,
82    Rolling { period: usize },
83}
84
85/// VWAP parameter pack.
86#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
87pub struct VwapParams {
88    pub mode: VwapMode,
89    pub price_source: VwapPriceSource,
90}
91
92impl VwapParams {
93    /// Cumulative VWAP on typical price — most common intraday default.
94    pub const fn cumulative_typical() -> Self {
95        Self {
96            mode: VwapMode::Cumulative,
97            price_source: VwapPriceSource::Typical,
98        }
99    }
100
101    pub const fn rolling_typical(period: usize) -> Self {
102        Self {
103            mode: VwapMode::Rolling { period },
104            price_source: VwapPriceSource::Typical,
105        }
106    }
107}
108
109/// Validated VWAP config.
110#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
111pub struct ValidatedVwap {
112    params: VwapParams,
113}
114
115impl ValidatedVwap {
116    pub fn new(params: VwapParams) -> FinanceResult<Self> {
117        if let VwapMode::Rolling { period } = params.mode {
118            PeriodLength::new(period)?;
119        }
120        Ok(Self { params })
121    }
122
123    pub fn params(self) -> VwapParams {
124        self.params
125    }
126
127    pub fn compute(
128        self,
129        high: &[f64],
130        low: &[f64],
131        close: &[f64],
132        volume: &[f64],
133    ) -> FinanceResult<VwapSeries> {
134        vwap_validated(high, low, close, volume, self)
135    }
136}
137
138#[derive(Clone, Debug, PartialEq)]
139pub struct VwapSeries {
140    pub typical: Vec<f64>,
141    pub vwap: Vec<Option<f64>>,
142    pub params: VwapParams,
143}
144
145#[derive(Clone, Debug)]
146pub struct VwapSolution {
147    series: VwapSeries,
148    volume: Vec<f64>,
149    formula: String,
150    symbolic_formula: String,
151}
152
153impl VwapSolution {
154    pub fn series(&self) -> &VwapSeries {
155        &self.series
156    }
157    pub fn formula(&self) -> &str {
158        &self.formula
159    }
160    pub fn symbolic_formula(&self) -> &str {
161        &self.symbolic_formula
162    }
163
164    /// # Sample output
165    /// ```text
166    /// period  typical  volume     vwap
167    /// ------  -------  ------  -------
168    ///      0   9.5000  100.00   9.5000
169    ///      1  10.5000  200.00  10.1667
170    /// ```
171    pub fn print_table(&self) {
172        self.print_table_locale_opt(None, None);
173    }
174
175    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
176        self.print_table_locale_opt(Some(locale), Some(precision));
177    }
178
179    fn print_table_locale_opt(
180        &self,
181        locale: Option<&num_format::Locale>,
182        precision: Option<usize>,
183    ) {
184        let columns = columns_with_strings(&[
185            ("period", "i", true),
186            ("typical", "f", true),
187            ("volume", "f", true),
188            ("vwap", "f", true),
189        ]);
190        let data = self
191            .series
192            .typical
193            .iter()
194            .enumerate()
195            .map(|(i, tp)| {
196                vec![
197                    i.to_string(),
198                    tp.to_string(),
199                    self.volume[i].to_string(),
200                    opt_cell(self.series.vwap[i]),
201                ]
202            })
203            .collect();
204        print_table_locale_opt(&columns, data, locale, precision);
205    }
206}
207
208pub fn vwap(
209    high: &[f64],
210    low: &[f64],
211    close: &[f64],
212    volume: &[f64],
213    params: VwapParams,
214) -> FinanceResult<VwapSeries> {
215    ValidatedVwap::new(params)?.compute(high, low, close, volume)
216}
217
218/// # Examples
219/// ```
220/// use finance_solution::stocks::ta::{vwap_solution, VwapParams};
221/// let h = [10.0, 11.0, 12.0];
222/// let l = [9.0, 10.0, 11.0];
223/// let c = [9.5, 10.5, 11.5];
224/// let v = [100.0, 200.0, 150.0];
225/// let sol = vwap_solution(&h, &l, &c, &v, VwapParams::cumulative_typical()).unwrap();
226/// assert!(sol.series().vwap[0].is_some());
227/// ```
228pub fn vwap_solution(
229    high: &[f64],
230    low: &[f64],
231    close: &[f64],
232    volume: &[f64],
233    params: VwapParams,
234) -> FinanceResult<VwapSolution> {
235    let series = vwap(high, low, close, volume, params)?;
236    let formula = match params.mode {
237        VwapMode::Cumulative => {
238            "vwap_t = sum_{i=0..t}(price_i * vol_i) / sum_{i=0..t}(vol_i)".to_string()
239        }
240        VwapMode::Rolling { period } => {
241            format!("vwap_t = sum(price*vol over last {period}) / sum(vol over last {period})")
242        }
243    };
244    let symbolic = "vwap = sum(price * volume) / sum(volume)".to_string();
245    Ok(VwapSolution {
246        series,
247        volume: volume.to_vec(),
248        formula,
249        symbolic_formula: symbolic,
250    })
251}
252
253fn vwap_validated(
254    high: &[f64],
255    low: &[f64],
256    close: &[f64],
257    volume: &[f64],
258    v: ValidatedVwap,
259) -> FinanceResult<VwapSeries> {
260    require_hlc(high, low, close)?;
261    validate_positive_volume(volume)?;
262    require_same_len(close, volume, "close/volume")?;
263    let p = v.params;
264    let n = close.len();
265    let mut typical = vec![0.0; n];
266    for i in 0..n {
267        typical[i] = match p.price_source {
268            VwapPriceSource::Typical => (high[i] + low[i] + close[i]) / 3.0,
269            VwapPriceSource::Close => close[i],
270        };
271    }
272    let mut vwap_out = vec![None; n];
273    match p.mode {
274        VwapMode::Cumulative => {
275            let mut cum_pv = 0.0;
276            let mut cum_v = 0.0;
277            for i in 0..n {
278                cum_pv += typical[i] * volume[i];
279                cum_v += volume[i];
280                if cum_v > 0.0 {
281                    vwap_out[i] = Some(cum_pv / cum_v);
282                }
283            }
284        }
285        VwapMode::Rolling { period } => {
286            for i in 0..n {
287                if i + 1 < period {
288                    continue;
289                }
290                let start = i + 1 - period;
291                let mut pv = 0.0;
292                let mut vv = 0.0;
293                for j in start..=i {
294                    pv += typical[j] * volume[j];
295                    vv += volume[j];
296                }
297                if vv > 0.0 {
298                    vwap_out[i] = Some(pv / vv);
299                }
300            }
301        }
302    }
303    Ok(VwapSeries {
304        typical,
305        vwap: vwap_out,
306        params: p,
307    })
308}
309
310#[cfg(test)]
311mod tests {
312    use super::*;
313
314    #[test]
315    fn cumulative_flat() {
316        let h = [10.0, 10.0];
317        let l = [10.0, 10.0];
318        let c = [10.0, 10.0];
319        let v = [100.0, 100.0];
320        let s = vwap(&h, &l, &c, &v, VwapParams::cumulative_typical()).unwrap();
321        assert!((s.vwap[1].unwrap() - 10.0).abs() < 1e-12);
322    }
323
324    #[test]
325    fn rolling_window() {
326        let h = [10.0, 12.0, 14.0, 16.0];
327        let l = [10.0, 12.0, 14.0, 16.0];
328        let c = [10.0, 12.0, 14.0, 16.0];
329        let v = [1.0, 1.0, 1.0, 1.0];
330        let s = vwap(&h, &l, &c, &v, VwapParams::rolling_typical(2)).unwrap();
331        assert!(s.vwap[0].is_none());
332        // bars 0-1 typical = 10, 12 → vwap = 11
333        assert!((s.vwap[1].unwrap() - 11.0).abs() < 1e-12);
334        // bars 2-3: 14, 16 → 15
335        assert!((s.vwap[3].unwrap() - 15.0).abs() < 1e-12);
336    }
337
338    #[test]
339    fn close_price_source() {
340        let h = [20.0, 20.0];
341        let l = [10.0, 10.0];
342        let c = [11.0, 13.0];
343        let v = [100.0, 100.0];
344        let p = VwapParams {
345            mode: VwapMode::Cumulative,
346            price_source: VwapPriceSource::Close,
347        };
348        let s = vwap(&h, &l, &c, &v, p).unwrap();
349        // equal volume → mid of closes
350        assert!((s.vwap[1].unwrap() - 12.0).abs() < 1e-12);
351    }
352
353    #[test]
354    fn zero_volume_stays_none_until_flow() {
355        let h = [10.0, 11.0];
356        let l = [10.0, 11.0];
357        let c = [10.0, 11.0];
358        let v = [0.0, 0.0];
359        let s = vwap(&h, &l, &c, &v, VwapParams::cumulative_typical()).unwrap();
360        assert!(s.vwap[0].is_none());
361        assert!(s.vwap[1].is_none());
362    }
363
364    #[test]
365    fn length_mismatch_err() {
366        let h = [10.0, 11.0];
367        let l = [9.0, 10.0];
368        let c = [9.5, 10.5];
369        let v = [100.0];
370        assert!(vwap(&h, &l, &c, &v, VwapParams::cumulative_typical()).is_err());
371    }
372}