1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
70pub enum VwapPriceSource {
71 #[default]
73 Typical,
74 Close,
76}
77
78#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
80pub enum VwapMode {
81 Cumulative,
82 Rolling { period: usize },
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
87pub struct VwapParams {
88 pub mode: VwapMode,
89 pub price_source: VwapPriceSource,
90}
91
92impl VwapParams {
93 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#[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 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
218pub 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 assert!((s.vwap[1].unwrap() - 11.0).abs() < 1e-12);
334 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 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}