use crate::stocks::ta::common::{
opt_cell, require_hlc, require_same_len, validate_positive_volume,
};
use crate::util::error::FinanceResult;
use crate::util::primitives::PeriodLength;
use crate::{columns_with_strings, print_table_locale_opt};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum VwapPriceSource {
#[default]
Typical,
Close,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum VwapMode {
Cumulative,
Rolling { period: usize },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct VwapParams {
pub mode: VwapMode,
pub price_source: VwapPriceSource,
}
impl VwapParams {
pub const fn cumulative_typical() -> Self {
Self {
mode: VwapMode::Cumulative,
price_source: VwapPriceSource::Typical,
}
}
pub const fn rolling_typical(period: usize) -> Self {
Self {
mode: VwapMode::Rolling { period },
price_source: VwapPriceSource::Typical,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ValidatedVwap {
params: VwapParams,
}
impl ValidatedVwap {
pub fn new(params: VwapParams) -> FinanceResult<Self> {
if let VwapMode::Rolling { period } = params.mode {
PeriodLength::new(period)?;
}
Ok(Self { params })
}
pub fn params(self) -> VwapParams {
self.params
}
pub fn compute(
self,
high: &[f64],
low: &[f64],
close: &[f64],
volume: &[f64],
) -> FinanceResult<VwapSeries> {
vwap_validated(high, low, close, volume, self)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct VwapSeries {
pub typical: Vec<f64>,
pub vwap: Vec<Option<f64>>,
pub params: VwapParams,
}
#[derive(Clone, Debug)]
pub struct VwapSolution {
series: VwapSeries,
volume: Vec<f64>,
formula: String,
symbolic_formula: String,
}
impl VwapSolution {
pub fn series(&self) -> &VwapSeries {
&self.series
}
pub fn formula(&self) -> &str {
&self.formula
}
pub fn symbolic_formula(&self) -> &str {
&self.symbolic_formula
}
pub fn print_table(&self) {
self.print_table_locale_opt(None, None);
}
pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
self.print_table_locale_opt(Some(locale), Some(precision));
}
fn print_table_locale_opt(
&self,
locale: Option<&num_format::Locale>,
precision: Option<usize>,
) {
let columns = columns_with_strings(&[
("period", "i", true),
("typical", "f", true),
("volume", "f", true),
("vwap", "f", true),
]);
let data = self
.series
.typical
.iter()
.enumerate()
.map(|(i, tp)| {
vec![
i.to_string(),
tp.to_string(),
self.volume[i].to_string(),
opt_cell(self.series.vwap[i]),
]
})
.collect();
print_table_locale_opt(&columns, data, locale, precision);
}
}
pub fn vwap(
high: &[f64],
low: &[f64],
close: &[f64],
volume: &[f64],
params: VwapParams,
) -> FinanceResult<VwapSeries> {
ValidatedVwap::new(params)?.compute(high, low, close, volume)
}
pub fn vwap_solution(
high: &[f64],
low: &[f64],
close: &[f64],
volume: &[f64],
params: VwapParams,
) -> FinanceResult<VwapSolution> {
let series = vwap(high, low, close, volume, params)?;
let formula = match params.mode {
VwapMode::Cumulative => {
"vwap_t = sum_{i=0..t}(price_i * vol_i) / sum_{i=0..t}(vol_i)".to_string()
}
VwapMode::Rolling { period } => {
format!("vwap_t = sum(price*vol over last {period}) / sum(vol over last {period})")
}
};
let symbolic = "vwap = sum(price * volume) / sum(volume)".to_string();
Ok(VwapSolution {
series,
volume: volume.to_vec(),
formula,
symbolic_formula: symbolic,
})
}
fn vwap_validated(
high: &[f64],
low: &[f64],
close: &[f64],
volume: &[f64],
v: ValidatedVwap,
) -> FinanceResult<VwapSeries> {
require_hlc(high, low, close)?;
validate_positive_volume(volume)?;
require_same_len(close, volume, "close/volume")?;
let p = v.params;
let n = close.len();
let mut typical = vec![0.0; n];
for i in 0..n {
typical[i] = match p.price_source {
VwapPriceSource::Typical => (high[i] + low[i] + close[i]) / 3.0,
VwapPriceSource::Close => close[i],
};
}
let mut vwap_out = vec![None; n];
match p.mode {
VwapMode::Cumulative => {
let mut cum_pv = 0.0;
let mut cum_v = 0.0;
for i in 0..n {
cum_pv += typical[i] * volume[i];
cum_v += volume[i];
if cum_v > 0.0 {
vwap_out[i] = Some(cum_pv / cum_v);
}
}
}
VwapMode::Rolling { period } => {
for i in 0..n {
if i + 1 < period {
continue;
}
let start = i + 1 - period;
let mut pv = 0.0;
let mut vv = 0.0;
for j in start..=i {
pv += typical[j] * volume[j];
vv += volume[j];
}
if vv > 0.0 {
vwap_out[i] = Some(pv / vv);
}
}
}
}
Ok(VwapSeries {
typical,
vwap: vwap_out,
params: p,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cumulative_flat() {
let h = [10.0, 10.0];
let l = [10.0, 10.0];
let c = [10.0, 10.0];
let v = [100.0, 100.0];
let s = vwap(&h, &l, &c, &v, VwapParams::cumulative_typical()).unwrap();
assert!((s.vwap[1].unwrap() - 10.0).abs() < 1e-12);
}
#[test]
fn rolling_window() {
let h = [10.0, 12.0, 14.0, 16.0];
let l = [10.0, 12.0, 14.0, 16.0];
let c = [10.0, 12.0, 14.0, 16.0];
let v = [1.0, 1.0, 1.0, 1.0];
let s = vwap(&h, &l, &c, &v, VwapParams::rolling_typical(2)).unwrap();
assert!(s.vwap[0].is_none());
assert!((s.vwap[1].unwrap() - 11.0).abs() < 1e-12);
assert!((s.vwap[3].unwrap() - 15.0).abs() < 1e-12);
}
#[test]
fn close_price_source() {
let h = [20.0, 20.0];
let l = [10.0, 10.0];
let c = [11.0, 13.0];
let v = [100.0, 100.0];
let p = VwapParams {
mode: VwapMode::Cumulative,
price_source: VwapPriceSource::Close,
};
let s = vwap(&h, &l, &c, &v, p).unwrap();
assert!((s.vwap[1].unwrap() - 12.0).abs() < 1e-12);
}
#[test]
fn zero_volume_stays_none_until_flow() {
let h = [10.0, 11.0];
let l = [10.0, 11.0];
let c = [10.0, 11.0];
let v = [0.0, 0.0];
let s = vwap(&h, &l, &c, &v, VwapParams::cumulative_typical()).unwrap();
assert!(s.vwap[0].is_none());
assert!(s.vwap[1].is_none());
}
#[test]
fn length_mismatch_err() {
let h = [10.0, 11.0];
let l = [9.0, 10.0];
let c = [9.5, 10.5];
let v = [100.0];
assert!(vwap(&h, &l, &c, &v, VwapParams::cumulative_typical()).is_err());
}
}