Files
wickra/docs/wiki/indicators/volume/Indicator-Vwap.md
T

294 lines
9.3 KiB
Markdown
Raw Normal View History

2026-05-22 16:18:04 +02:00
# VWAP (Volume-Weighted Average Price)
> The volume-weighted mean of typical price; the institutional benchmark for
> "fair" intraday execution. Wickra ships both the unbounded cumulative
> session VWAP and a finite-window `RollingVwap`.
This page documents two distinct public types — jump straight to
[`Vwap` (cumulative)](#vwap-cumulative) or
[`RollingVwap` (finite window)](#rollingvwap-finite-window).
2026-05-22 16:18:04 +02:00
## Quick reference
| Item | Value |
|---------------------|----------------------------------------------------------------|
| Family | Volume |
2026-05-22 16:18:04 +02:00
| Input type | `Candle` (uses `high`, `low`, `close`, `volume`) |
| Output type | `f64` |
| Output range | unbounded (price-units) |
| Default parameters | none for `Vwap`; `period` required for `RollingVwap` |
| Warmup period | `1` for `Vwap`, `period` for `RollingVwap` |
| Interpretation | intraday fair-price benchmark for execution |
## Formula
Both variants use the typical price `tp_t = (H_t + L_t + C_t) / 3`
(see `Candle::typical_price` in `crates/wickra-core/src/ohlcv.rs:104-108`).
Cumulative VWAP:
```
VWAP_t = ( Σ_{i=1..t} tp_i * v_i ) / ( Σ_{i=1..t} v_i )
```
Rolling VWAP over the last `period` candles:
```
RollingVWAP_t = ( Σ_{i=t-period+1..t} tp_i * v_i ) / ( Σ_{i=t-period+1..t} v_i )
```
Both forms gate their output: when the relevant volume sum is `0.0`, no
value is emitted (`vwap.rs:50, 121`).
---
## `Vwap` (cumulative)
The session VWAP. State grows forever; call `reset()` at session
boundaries (e.g. the start of the trading day) to restart accumulation.
### Parameters
`Vwap::new()` takes no parameters. Python: `wickra.VWAP()`. Node:
`new w.VWAP()`.
### Inputs / Outputs
```rust
impl Indicator for Vwap {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64>;
fn warmup_period(&self) -> usize { 1 }
}
```
- **Rust input.** A full `Candle`; the indicator multiplies
`typical_price() * volume` and accumulates.
- **Python batch.** `VWAP.batch(high, low, close, volume)` returns a 1-D
`np.ndarray` with `NaN` for any prefix where the cumulative volume is
still `0`.
- **Node batch.** `vwap.batch(high, low, close, volume)` returns
`Array<number>` with `NaN` for the same prefix.
### Warmup
`warmup_period() == 1`. Provided the first candle has positive volume,
the indicator emits on tick 1. If the first `k` candles all have
`volume == 0`, no output is emitted until the first candle with
non-zero volume — `RollingVwap`'s warmup gating is independent of
this volume-gating logic and applies on top of it.
### Edge cases
- **Zero-volume bar.** A candle with `volume == 0` does not advance the
running sums in any visible way and (if it is the *first* such bar
the indicator has seen) keeps the output at `None`. The implementation
short-circuits with `if self.sum_v == 0.0 { return None; }`
(`vwap.rs:50`).
- **Constant input.** Identical candles produce a flat VWAP equal to
their typical price.
- **Session boundaries.** There is no automatic reset; the caller is
responsible for invoking `reset()` at the start of each new session.
- **NaN / infinity.** `Candle::new` rejects non-finite OHLCV values
before they can reach the indicator.
- **Reset.** `reset()` zeroes both running sums and unsets the `has_emitted`
flag.
### Examples
#### Rust
```rust
use wickra::{BatchExt, Candle, Indicator, Vwap};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let candles = vec![
Candle::new(10.0, 10.0, 10.0, 10.0, 1.0, 0)?, // tp = 10
Candle::new(20.0, 20.0, 20.0, 20.0, 3.0, 0)?, // tp = 20
Candle::new(30.0, 30.0, 30.0, 30.0, 1.0, 0)?, // tp = 30
Candle::new(40.0, 40.0, 40.0, 40.0, 2.0, 0)?, // tp = 40
];
let mut v = Vwap::new();
println!("{:?}", v.batch(&candles));
Ok(())
}
```
Output:
```
[Some(10.0), Some(17.5), Some(20.0), Some(25.714285714285715)]
```
Hand check at `t = 2`: `(10*1 + 20*3) / (1+3) = 70/4 = 17.5`.
At `t = 4`: `(10*1 + 20*3 + 30*1 + 40*2) / (1+3+1+2) = 180/7 ≈ 25.7142857`.
#### Python
```python
import numpy as np
import wickra as ta
vw = ta.VWAP()
h = np.array([10.0, 20.0, 30.0, 40.0])
l = np.array([10.0, 20.0, 30.0, 40.0])
c = np.array([10.0, 20.0, 30.0, 40.0])
v = np.array([ 1.0, 3.0, 1.0, 2.0])
print(vw.batch(h, l, c, v))
```
Output:
```
[10. 17.5 20. 25.71428571]
```
#### Node
```js
const w = require('wickra');
const vw = new w.VWAP();
console.log(vw.batch(
[10, 20, 30, 40],
[10, 20, 30, 40],
[10, 20, 30, 40],
[ 1, 3, 1, 2],
));
```
Output:
```
[ 10, 17.5, 20, 25.714285714285715 ]
```
---
## `RollingVwap` (finite window)
A rolling-window variant for streaming bots that want a finite-memory
fair-price benchmark instead of an unbounded session aggregate.
### Parameters
| Name | Type | Default | Constraint | Source |
|----------|---------|--------------|------------|----------------------------------------------|
| `period` | `usize` | (no default) | `> 0` | `RollingVwap::new` (`vwap.rs:89`) |
`period == 0` returns `Error::PeriodZero`. `RollingVwap` is exposed in
Rust only — Python's `VWAP` / Node's `VWAP` correspond to the cumulative
form.
### Inputs / Outputs
```rust
impl Indicator for RollingVwap {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64>;
fn warmup_period(&self) -> usize { self.period }
}
```
The window stores `(typical_price * volume, volume)` pairs and runs
incremental `sum_pv` / `sum_v` aggregates, so each `update` is O(1).
### Warmup
`warmup_period() == period`. The first `period - 1` candles return
`None`; the `period`-th candle emits the first value provided the rolling
volume sum is positive. If the entire window has `volume == 0`, the
indicator stays at `None`.
### Edge cases
- **Window slides.** Once `window.len() == period`, the oldest
`(pv, v)` pair is subtracted from the running sums before the new
pair is added.
- **Zero-volume window.** If every candle in the window has zero
volume, `sum_v == 0` and the indicator suppresses output until a
positive-volume candle is in scope.
- **Reset.** `reset()` clears the window and both running sums.
- **`is_ready()`.** Returns `true` only when the window is full **and**
`sum_v > 0` (`vwap.rs:138`).
### Examples
#### Rust
```rust
use wickra::{BatchExt, Candle, Indicator, RollingVwap};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let candles = vec![
Candle::new(10.0, 10.0, 10.0, 10.0, 1.0, 0)?,
Candle::new(20.0, 20.0, 20.0, 20.0, 3.0, 0)?,
Candle::new(30.0, 30.0, 30.0, 30.0, 1.0, 0)?,
Candle::new(40.0, 40.0, 40.0, 40.0, 2.0, 0)?,
];
let mut rv = RollingVwap::new(3)?;
println!("{:?}", rv.batch(&candles));
Ok(())
}
```
Output:
```
[None, None, Some(20.0), Some(28.333333333333332)]
```
Hand check at `t = 3` with window `[10@1, 20@3, 30@1]`:
`(10 + 60 + 30) / (1+3+1) = 100/5 = 20.0`.
At `t = 4` with window `[20@3, 30@1, 40@2]`:
`(60 + 30 + 80) / (3+1+2) = 170/6 ≈ 28.333`.
(`RollingVwap` is currently exposed only in the Rust API; the Python
`VWAP` and Node `VWAP` classes correspond to the cumulative form.)
## Interpretation
- **Execution benchmark.** "Beat VWAP" is the canonical buy-side
execution mandate: an aggressive algo that ends up paying *below*
VWAP on the day is considered to have earned alpha relative to a
passive participation strategy.
- **Mean reversion.** Intraday strategies often fade extensions away
from VWAP, treating the VWAP line as a magnet.
- **Trend filter.** Some systems trade only longs above VWAP and only
shorts below it; the line acts as a session-aware bias toggle.
## Common pitfalls
- **Forgetting to reset.** Call `reset()` at session start (or on each
new trading day) — otherwise you average yesterday's tape into
today's signal and the line drifts permanently behind current
price action.
- **Zero-volume warmup.** Several common data sources include
pre-session candles with `volume = 0` for "no print this minute".
Cumulative VWAP returns `None` until at least one positive-volume
candle has been seen; downstream code should treat `None` /
`NaN` / `null` as "not yet ready," not as "VWAP is zero."
- **Typical price vs close.** Wickra uses typical price
`(H + L + C) / 3`, not close. A naive implementation that uses
close will produce noticeably different numbers on bars with wide
intraday ranges.
## References
- The VWAP construct emerged in institutional execution literature in
the late 1980s and early 1990s; it has no single attributed
inventor. The textbook reference for its role as an execution
benchmark is Bertsimas & Lo, "Optimal control of execution costs,"
*Journal of Financial Markets*, 1998.
## See also
- [OBV](../volume/Indicator-Obv.md) — cumulative signed-volume measure that pairs
2026-05-22 16:18:04 +02:00
well with VWAP as a divergence flag.
- [MFI](../momentum-oscillators/Indicator-Mfi.md) — money-flow oscillator that also blends
2026-05-22 16:18:04 +02:00
typical price with volume.
- [Bollinger Bands](../volatility-bands/Indicator-BollingerBands.md) — non-volume volatility
2026-05-22 16:18:04 +02:00
envelope, often layered alongside VWAP on intraday charts.