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::opt_cell;
62use crate::util::error::FinanceResult;
63use crate::util::primitives::PeriodLength;
64use crate::{columns_with_strings, print_table_locale_opt};
65
66/// Price input for VWAP numerator.
67#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
68pub enum VwapPriceSource {
69    /// `(high + low + close) / 3`.
70    #[default]
71    Typical,
72    /// Close only.
73    Close,
74}
75
76/// Cumulative session vs rolling window.
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
78pub enum VwapMode {
79    Cumulative,
80    Rolling { period: usize },
81}
82
83/// VWAP parameter pack.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
85pub struct VwapParams {
86    pub mode: VwapMode,
87    pub price_source: VwapPriceSource,
88}
89
90impl VwapParams {
91    /// Cumulative VWAP on typical price — most common intraday default.
92    pub const fn cumulative_typical() -> Self {
93        Self {
94            mode: VwapMode::Cumulative,
95            price_source: VwapPriceSource::Typical,
96        }
97    }
98
99    pub const fn rolling_typical(period: usize) -> Self {
100        Self {
101            mode: VwapMode::Rolling { period },
102            price_source: VwapPriceSource::Typical,
103        }
104    }
105}
106
107/// Validated VWAP config.
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
109pub struct ValidatedVwap {
110    params: VwapParams,
111}
112
113impl ValidatedVwap {
114    pub fn new(params: VwapParams) -> FinanceResult<Self> {
115        if let VwapMode::Rolling { period } = params.mode {
116            PeriodLength::new(period)?;
117        }
118        Ok(Self { params })
119    }
120
121    pub fn params(self) -> VwapParams {
122        self.params
123    }
124
125    pub fn compute(
126        self,
127        high: &[f64],
128        low: &[f64],
129        close: &[f64],
130        volume: &[f64],
131    ) -> FinanceResult<VwapSeries> {
132        vwap_validated(high, low, close, volume, self)
133    }
134}
135
136#[derive(Clone, Debug, PartialEq)]
137pub struct VwapSeries {
138    pub typical: Vec<f64>,
139    pub vwap: Vec<Option<f64>>,
140    pub params: VwapParams,
141}
142
143#[derive(Clone, Debug)]
144pub struct VwapSolution {
145    series: VwapSeries,
146    volume: Vec<f64>,
147    formula: String,
148    symbolic_formula: String,
149}
150
151impl VwapSolution {
152    pub fn series(&self) -> &VwapSeries {
153        &self.series
154    }
155    pub fn formula(&self) -> &str {
156        &self.formula
157    }
158    pub fn symbolic_formula(&self) -> &str {
159        &self.symbolic_formula
160    }
161
162    /// # Sample output
163    /// ```text
164    /// period  typical  volume     vwap
165    /// ------  -------  ------  -------
166    ///      0   9.5000  100.00   9.5000
167    ///      1  10.5000  200.00  10.1667
168    /// ```
169    pub fn print_table(&self) {
170        self.print_table_locale_opt(None, None);
171    }
172
173    pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
174        self.print_table_locale_opt(Some(locale), Some(precision));
175    }
176
177    fn print_table_locale_opt(
178        &self,
179        locale: Option<&num_format::Locale>,
180        precision: Option<usize>,
181    ) {
182        let columns = columns_with_strings(&[
183            ("period", "i", true),
184            ("typical", "f", true),
185            ("volume", "f", true),
186            ("vwap", "f", true),
187        ]);
188        let data = self
189            .series
190            .typical
191            .iter()
192            .enumerate()
193            .map(|(i, tp)| {
194                vec![
195                    i.to_string(),
196                    tp.to_string(),
197                    self.volume[i].to_string(),
198                    opt_cell(self.series.vwap[i]),
199                ]
200            })
201            .collect();
202        print_table_locale_opt(&columns, data, locale, precision);
203    }
204}
205
206pub fn vwap(
207    high: &[f64],
208    low: &[f64],
209    close: &[f64],
210    volume: &[f64],
211    params: VwapParams,
212) -> FinanceResult<VwapSeries> {
213    ValidatedVwap::new(params)?.compute(high, low, close, volume)
214}
215
216/// # Examples
217/// ```
218/// use finance_solution::stocks::ta::{vwap_solution, VwapParams};
219/// let h = [10.0, 11.0, 12.0];
220/// let l = [9.0, 10.0, 11.0];
221/// let c = [9.5, 10.5, 11.5];
222/// let v = [100.0, 200.0, 150.0];
223/// let sol = vwap_solution(&h, &l, &c, &v, VwapParams::cumulative_typical()).unwrap();
224/// assert!(sol.series().vwap[0].is_some());
225/// ```
226pub fn vwap_solution(
227    high: &[f64],
228    low: &[f64],
229    close: &[f64],
230    volume: &[f64],
231    params: VwapParams,
232) -> FinanceResult<VwapSolution> {
233    let series = vwap(high, low, close, volume, params)?;
234    let formula = match params.mode {
235        VwapMode::Cumulative => {
236            "vwap_t = sum_{i=0..t}(price_i * vol_i) / sum_{i=0..t}(vol_i)".to_string()
237        }
238        VwapMode::Rolling { period } => {
239            format!("vwap_t = sum(price*vol over last {period}) / sum(vol over last {period})")
240        }
241    };
242    let symbolic = "vwap = sum(price * volume) / sum(volume)".to_string();
243    Ok(VwapSolution {
244        series,
245        volume: volume.to_vec(),
246        formula,
247        symbolic_formula: symbolic,
248    })
249}
250
251fn vwap_validated(
252    high: &[f64],
253    low: &[f64],
254    close: &[f64],
255    volume: &[f64],
256    v: ValidatedVwap,
257) -> FinanceResult<VwapSeries> {
258    let p = v.params;
259    let n = close.len();
260    let mut typical = Vec::with_capacity(n);
261    for i in 0..n {
262        // Typical is a pure bar transform for tables; VWAP math lives in VwapState.
263        let t = match p.price_source {
264            VwapPriceSource::Typical => (high[i] + low[i] + close[i]) / 3.0,
265            VwapPriceSource::Close => close[i],
266        };
267        typical.push(t);
268    }
269    let mut st = crate::stocks::ta::state::VwapState::new(p)?;
270    let vwap_out = st.push_bars(high, low, close, volume)?;
271    Ok(VwapSeries {
272        typical,
273        vwap: vwap_out,
274        params: p,
275    })
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn cumulative_flat() {
284        let h = [10.0, 10.0];
285        let l = [10.0, 10.0];
286        let c = [10.0, 10.0];
287        let v = [100.0, 100.0];
288        let s = vwap(&h, &l, &c, &v, VwapParams::cumulative_typical()).unwrap();
289        assert!((s.vwap[1].unwrap() - 10.0).abs() < 1e-12);
290    }
291
292    #[test]
293    fn rolling_window() {
294        let h = [10.0, 12.0, 14.0, 16.0];
295        let l = [10.0, 12.0, 14.0, 16.0];
296        let c = [10.0, 12.0, 14.0, 16.0];
297        let v = [1.0, 1.0, 1.0, 1.0];
298        let s = vwap(&h, &l, &c, &v, VwapParams::rolling_typical(2)).unwrap();
299        assert!(s.vwap[0].is_none());
300        // bars 0-1 typical = 10, 12 → vwap = 11
301        assert!((s.vwap[1].unwrap() - 11.0).abs() < 1e-12);
302        // bars 2-3: 14, 16 → 15
303        assert!((s.vwap[3].unwrap() - 15.0).abs() < 1e-12);
304    }
305
306    #[test]
307    fn close_price_source() {
308        let h = [20.0, 20.0];
309        let l = [10.0, 10.0];
310        let c = [11.0, 13.0];
311        let v = [100.0, 100.0];
312        let p = VwapParams {
313            mode: VwapMode::Cumulative,
314            price_source: VwapPriceSource::Close,
315        };
316        let s = vwap(&h, &l, &c, &v, p).unwrap();
317        // equal volume → mid of closes
318        assert!((s.vwap[1].unwrap() - 12.0).abs() < 1e-12);
319    }
320
321    #[test]
322    fn zero_volume_stays_none_until_flow() {
323        let h = [10.0, 11.0];
324        let l = [10.0, 11.0];
325        let c = [10.0, 11.0];
326        let v = [0.0, 0.0];
327        let s = vwap(&h, &l, &c, &v, VwapParams::cumulative_typical()).unwrap();
328        assert!(s.vwap[0].is_none());
329        assert!(s.vwap[1].is_none());
330    }
331
332    #[test]
333    fn length_mismatch_err() {
334        let h = [10.0, 11.0];
335        let l = [9.0, 10.0];
336        let c = [9.5, 10.5];
337        let v = [100.0];
338        assert!(vwap(&h, &l, &c, &v, VwapParams::cumulative_typical()).is_err());
339    }
340}