Volume and VWAP

View as MarkdownOpen the editor

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 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.

TypeWhat 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 page; for per-price buy and sell volume inside one bar see Order flow.

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.

AnchorBoundaryFirst finite value
"" (none)never; one accumulation from the first loaded barthe first bar
"day"every 00:00 UTCthe first bar
"week"every Monday 00:00 UTCthe first bar
"month", "quarter", "year"the first of the period, 00:00 UTCthe first bar
"14400000" (any number of milliseconds, as a string)every bucket of that width, floored from the epochthe 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.

wrun
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) 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").

wrun
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):

wrun
// 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).

wrun
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).

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.

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);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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.

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);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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.

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);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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 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.