---
title: "Volume and VWAP"
description: "Volume tools fold volume into the calculation to read buying and selling pressure, participation, and the price volume actually paid. Three ship as classes in…"
order: 45
section: "functions"
---

<!-- source: docs/indicators/functions/volume-indicators.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Volume and VWAP

Volume tools fold volume into the calculation to read buying and selling
pressure, participation, and the price volume actually paid. Three ship
as classes in the editor's `./sdk/ta` module (`Obv`, `Vwap`, `Cum`); the
money flow index is an oscillator and lives on the
[Oscillators](oscillators.md#mfi) page. Volume itself is the bar's own
field (`bar.volume()`), and the buy and sell split is two `trades.volume`
inputs with a `side`.

| Type | What it reads |
| --- | --- |
| On-balance volume (`Obv`) | a cumulative line that adds volume on up bars and subtracts it on down bars |
| Volume-weighted average price (`Vwap`) | the fair price weighted by where volume traded, cumulative or reset on a calendar anchor |
| Cumulative sum (`Cum`) | the running total of any series, the primitive behind OBV-style accumulation |

For volume-weighted moving averages see `Vwma` on the
[Moving averages](moving-averages.md) page; for per-price buy and sell
volume inside one bar see [Order flow](order-flow-kit.md).

## Obv

`new Obv()`, `.update(close, volume)`. A running cumulative line with no
period: bar 0 returns `0`, then the bar's volume is added when the close
rose against the previous close, subtracted when it fell, and ignored when
equal (or when either close is `NaN`). The whole state is the previous
close and the running total, which `reset()` clears. Its level depends on
how much history the chart loaded; its slope does not, and the slope is
what confirms or contradicts price.

## Vwap

`new Vwap(anchor = "", price = "hlc3")`, `.update(open, high, low, close,
volume, tsMs)` returns the running VWAP: price times volume over volume,
accumulated from an anchor and reset at each boundary. `price` picks the
bar price: `hlc3`, `hl2`, `ohlc4` (the only mode that reads the open),
`hlcc4`, or `close`. `tsMs` is the bar's open time in milliseconds since
the epoch and is only read when an anchor is set: `bar.time()` delivers
seconds, so pass `bar.time() * 1000.0`.

| Anchor | Boundary | First finite value |
| --- | --- | --- |
| `""` (none) | never; one accumulation from the first loaded bar | the first bar |
| `"day"` | every 00:00 UTC | the first bar |
| `"week"` | every Monday 00:00 UTC | the first bar |
| `"month"`, `"quarter"`, `"year"` | the first of the period, 00:00 UTC | the first bar |
| `"14400000"` (any number of milliseconds, as a string) | every bucket of that width, floored from the epoch | the first bar |

**No leading gap, and anchored lines are stable.** The engine starts a
bucket on the first bar of the series whatever the calendar says, so an
anchored line is finite from bar 0 and its first period is partial:
inside it the level depends on where the loaded history starts, and from
the first boundary on every period computes only from its own bars, so
loading older history cannot change later buckets. The no-anchor form is
partial for the whole series (two charts with different history depths
disagree, and the level shifts when history loads), which is why the
anchored forms are the ones to share.

**Engine conventions.** A bar whose high, low, close or volume is not
finite (or open, for `"ohlc4"`) marks the current bucket invalid: the
value is `NaN` from that bar until the next bucket starts, forever in the
no-anchor form. A zero total volume is `NaN`. A `NaN` time with an anchor
set clears the sums and returns `NaN`. The no-anchor, `"day"` and
numeric-millisecond anchors match the engine bit for bit on the reference
window; `"week"`, `"month"`, `"quarter"` and `"year"` are computed from
the same calendar arithmetic but unproven on that window. Not mirrored,
because a per-bar class never sees the data: the engine's
session-calendar bucketing on venues with a trading calendar, its
regular-trading-hours filter, and the extra history it loads for quarter
and year anchors.

```typescript
const weekly = new Vwap("week");             // hlc3 price, resets every Monday 00:00 UTC
const session = new Vwap("14400000", "close"); // four-hour buckets over the close

// In onBar(): bar.time() is epoch seconds, the class wants milliseconds.
const tMs = bar.time() * 1000.0;
const weekValue = weekly.update(bar.open(), bar.high(), bar.low(), bar.close(), bar.volume(), tMs);
```

**Any source.** The price is one of the five named ones. Pine's
`ta.vwap(source)` takes any series: `SourceVwap(anchor)` in
`./sdk/ta-plus` ([Extra indicators](extra-indicators.md)) keeps the same
sums and anchors over a source you compute, `update(source, volume,
tsMs)`; fed `(high + low + close) / 3` it is `Vwap(anchor, "hlc3")`.

```typescript
const daily = new SourceVwap("day");
// In onBar(): the session VWAP of the bar's ohlc4.
const o = bar.open(), h = bar.high(), l = bar.low(), c = bar.close();
const value = daily.update((o + h + l + c) / 4.0, bar.volume(), bar.time() * 1000.0);
```

**Custom anchors.** The anchor is a bucket id computed from the bar's open
time: a UTC day index for `"day"`, a week index whose origin is three days
before the epoch (so weeks start on Monday) for `"week"`, and civil
calendar math for the month, quarter and year forms. When the id changes
the sums reset, and the same reset-on-id rule is a few lines of arithmetic
for any anchor the class does not name (a session open, a news bar, a
manual level):

```typescript
// The same rule the class applies, written out for a custom anchor.
const DAY_MS: f64 = 86400000.0;
const dayId = Math.floor(tMs / DAY_MS);                  // "day"
const weekId = Math.floor((tMs + 3.0 * DAY_MS) / (7.0 * DAY_MS)); // "week", Monday origin
const bucketId = Math.floor(tMs / 14400000.0);            // "14400000"
```

Assign the class to a module-level `let` like any other; there is nothing
to wait for, the line is finite on the first bar with volume.

## Cum

`new Cum()`, `.update(x)`. It is barely a class: a running total of
whatever you feed it from the start of the data, bar 0 returning `x`
itself. It is the primitive behind OBV-style accumulation and custom
anchored math (feed it signed volume for a delta proxy).

```typescript
const cumDelta = new Cum();

function onBar(): void {
  const volume = bar.volume();
  // Signed volume: the bar's volume counted toward the side of its close.
  const deltaValue = cumDelta.update(bar.close() >= bar.open() ? volume : -volume);
  out_cum_delta(deltaValue);
}
```

A non-finite sample sets the running total to `NaN` for the rest of the
history, which is the engine's `cum` rule; a sparse input that must not
poison it goes through `Fixnan` first, or through a `missing: "zero"`
policy on the source ([Series functions](series-functions.md)).

## Putting them together

The four on one lower pane plus the two VWAP lines on the price pane: a
money flow index, on-balance volume, a cumulative signed-volume delta
proxy, and the cumulative and daily VWAPs.

```typescript sample=fn-volume-kit
param("mfi_period", 14, { min: 2, max: 200 });
output("vwap_cum", line, overlay, { color: "#2563eb", width: 1, description: "Cumulative VWAP from the first loaded bar" });
output("vwap_day", line, overlay, { color: "#eab308", width: 2, description: "VWAP reset at each UTC day boundary" });
output("mfi", line, lower, { color: "#16a34a", width: 2, description: "Money flow index, 0..100" });
output("obv", line, lower, { color: "#0f766e", width: 1, description: "On-balance volume" });
output("cum_delta", line, lower, { color: "#22d3ee", width: 1, description: "Cumulative signed volume: a delta proxy" });

let mfi = new Mfi(14);
const obv = new Obv();
const cumDelta = new Cum();
const vwapCum = new Vwap();
const vwapDay = new Vwap("day");

function onStart(): void {
  mfi = new Mfi(i32(p_mfi_period()));
}

function onBar(): void {
  const close = bar.close();
  const high = bar.high();
  const low = bar.low();
  const volume = bar.volume();
  const mfiValue = mfi.update(high, low, close, volume);
  const obvValue = obv.update(close, volume);
  const open = bar.open();
  // Signed volume: the bar's volume counted toward the side of its close.
  const deltaValue = cumDelta.update(close >= open ? volume : -volume);
  const tsMs = bar.time() * 1000.0;
  const vwapCumValue = vwapCum.update(open, high, low, close, volume, tsMs);
  const vwapDayValue = vwapDay.update(open, high, low, close, volume, tsMs);
  out_vwap_cum(vwapCumValue);
  out_vwap_day(vwapDayValue);
  out_mfi(mfiValue);
  out_obv(obvValue);
  out_cum_delta(deltaValue);
}
```

## VWAP anchors side by side

The four VWAP variants side by side. Every line is finite from the first
bar; the anchored ones reset at their boundary and are partial before the
first one. `day_start` is a data-only flag that is `1` on the bar that
opens a new UTC day, the bar where the daily sums reset, so the boundary is
a value you can read after a run or hand to a declared alert.

```typescript sample=fn-anchored-vwap
input("open", ohlcv.open);
output("vwap_cum", line, overlay, { color: "#2563eb", width: 2, description: "VWAP with no anchor, from the first loaded bar" });
output("vwap_day", line, overlay, { color: "#16a34a", width: 2, description: "VWAP anchored to the UTC day" });
output("vwap_week", line, overlay, { color: "#f97316", width: 2, description: "VWAP anchored to the UTC week" });
output("vwap_month", line, overlay, { color: "#7c3aed", width: 2, description: "VWAP anchored to the UTC month" });
output("day_start", none, overlay, { description: "1 on the bar that opens a new UTC day" });

const DAY_MS: f64 = 86400000.0;
const cumulative = new Vwap("");
const daily = new Vwap("day");
const weekly = new Vwap("week");
const monthly = new Vwap("month");
let prevDay: f64 = NaN;

function onBar(): void {
  const open = bar.open();
  const high = bar.high();
  const low = bar.low();
  const close = bar.close();
  const volume = bar.volume();
  const tMs = bar.time() * 1000.0; // bar.time() is seconds; the class wants milliseconds
  const day = Math.floor(tMs / DAY_MS);
  const dayStart = !isNaN(prevDay) && day != prevDay ? 1.0 : 0.0;
  prevDay = day;
  const cumValue = cumulative.update(open, high, low, close, volume, tMs);
  const dayValue = daily.update(open, high, low, close, volume, tMs);
  const weekValue = weekly.update(open, high, low, close, volume, tMs);
  const monthValue = monthly.update(open, high, low, close, volume, tMs);
  out_vwap_cum(cumValue);
  out_vwap_day(dayValue);
  out_vwap_week(weekValue);
  out_vwap_month(monthValue);
  out_day_start(dayStart);
}
```

## Seeing the VWAP anchor boundary

To see exactly where each anchored line resets, write a flag that is
`1` on the bar whose bucket id differs from the previous bar's, the way
`day_start` does above for the daily line. The flag is an output, so
after a **Run** the editor's Console prompt reads it without drawing it:
`last 200 day_start` lists the newest 200 bars, and `at <ISO time>
day_start` reads one bar. To see the boundaries on the chart instead,
declare the flag as a `histogram` on `lower` while you check, then set it
back to `none`.

## Buy and sell volume

The close-versus-open sign above is a proxy. The real side split is a
source: `trades` serves per-period volume aggregated by aggressor side,
one series per side, so buy and sell volume are two inputs on the same
feed with a `side`. A venue that skips bars on
one side delivers nothing on that bar; `missing: "zero"` turns the gap
into `0` so the delta stays finite. The chart serves `trades` as per-bar
volume by side (`volume` with `side: "BUY"` or `"SELL"`, in base-asset
units) over the loaded history and live; any other `trades` field is
refused by name.

```typescript sample=fn-side-volume
input("close", ohlcv.close);
input("buy", trades.volume, { side: "BUY", missing: "zero", description: "Volume traded by aggressive buyers" });
input("sell", trades.volume, { side: "SELL", missing: "zero", description: "Volume traded by aggressive sellers" });
output("delta", histogram, lower, { color_by: "delta_sign", colors: ["#ef5350", "#26a69a"], description: "Buy minus sell volume per bar" });
output("delta_sign", none, lower, { description: "0 on a sell-dominant bar, 1 on a buy-dominant bar" });
output("cvd", line, lower, { color: "#22d3ee", width: 2, description: "Cumulative volume delta" });

let cvd: f64 = 0.0;

function onBar(): void {
  const delta = in_buy() - in_sell();
  cvd += delta;
  out_delta(delta);
  out_delta_sign(delta >= 0.0 ? 1.0 : 0.0);
  out_cvd(cvd);
}
```

The primary input stays the close so the wrun indicator follows the chart's
market and interval; the two side inputs align to its grid row for row.
Side volume always comes from the chart's own market: the chart refuses
a `symbol` + `exchange` pin on a `trades` input ("the browser lane serves
market pins on secondary ohlcv inputs only (typed feeds, cells and time
follow the chart's own market)"). The
[aggregated CVD recipe](../cookbook/aggregated-cvd.md) builds on the same
two inputs.

## Reading them

- **Volume quality.** High volume on a breakout confirms the move; a
  low-volume breakout often reverses. `delta` and `cvd` say which side did
  the volume, not just how much.
- **Divergence.** Price at a new high while `obv` or `cvd` is not is a
  warning.
- **Anchors.** The daily VWAP is the intraday fair price; the cumulative
  line is context that drifts with the loaded window.
