finance_solution/stocks/ta/
vwap.rs1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
68pub enum VwapPriceSource {
69 #[default]
71 Typical,
72 Close,
74}
75
76#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
78pub enum VwapMode {
79 Cumulative,
80 Rolling { period: usize },
81}
82
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
85pub struct VwapParams {
86 pub mode: VwapMode,
87 pub price_source: VwapPriceSource,
88}
89
90impl VwapParams {
91 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#[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 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
216pub 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 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 assert!((s.vwap[1].unwrap() - 11.0).abs() < 1e-12);
302 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 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}