---
title: "Volume indicators"
description: "Volume indicators fold volume into the calculation to read buying and selling pressure, money flow, and participation. kScript (legacy) shipped mfi, obv, vwap…"
order: 36
section: "functions"
---

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

# Volume indicators

Volume indicators fold volume into the calculation to read buying and
selling pressure, money flow, and participation. kScript (legacy) shipped
`mfi`, `obv`, `vwap`, and `cum` as builtins; all four ship as classes in
`src/sdk/ta.ts` (`Mfi`, `Obv`, `Vwap`, `Cum`), each matching the kScript
engine bar for bar. Volume itself is an ordinary input (`input("volume",
ohlcv.volume)`), and the side split the kScript `buy_sell_volume` source
gave you is two `trades.volume` inputs with a `side`.

| Type | What it reads |
| --- | --- |
| Money flow index (`Mfi`) | a volume-weighted RSI, 0..100; overbought above 80, oversold below 20 |
| 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 anchored |
| 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 [Volume profile](volume-profile.md).

## Mfi

`new Mfi(period = 14)`, `.update(high, low, close, volume)`. Each bar's
raw money flow is the typical price `(high + low + close) / 3` times
volume, counted as positive when the typical price rose against the
previous bar, negative when it fell, and neither when equal; the index is
`100 - 100 / (1 + positive / negative)` over the last `period` bars, and
`100` when the negative sum is `0`. Like `Rsi`, but weighted by volume,
so it reads pressure rather than price alone. The first value lands at
bar `period`; a non-finite bar fails both comparisons and adds nothing,
as in the engine.

## 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 host 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)`. `anchor` is `""` for one accumulation from the first bar
(what `vwap()` computes), `"day"`, `"week"`, `"month"`, `"quarter"`, or
`"year"` to restart the sums at each UTC calendar boundary (a week starts
Monday 00:00 UTC), or a number of milliseconds as a string
(`"14400000"`) for a fixed bucket width floored from the epoch. `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: the `time` source
delivers seconds, so pass `in_bar_t() * 1000.0`. Three properties worth
knowing:

1. **Anchored VWAP is stable under history loading.** Each bucket
   computes only from its own bars, so loading older history cannot
   change later buckets' values. The unanchored cumulative form does
   shift when history loads: its anchor is the data edge.
2. **There is no warm-up.** The first bar of the series starts a bucket,
   even a partial one, so a load that begins mid-session shows a value
   from the first bar, computed from the bars it has; the value settles
   once a full bucket is in view. A bar whose high, low, close, or volume
   is not finite marks the current bucket `NaN` until the next bucket
   starts (forever, in the cumulative form), and a zero total volume gives
   `NaN`.
3. **Session calendars are not mirrored.** The engine's session-calendar
   bucketing on venues with trading sessions, its regular-hours filter,
   and its deep-history lane for quarter and year anchors need data a
   per-bar class never sees; the calendar anchors above are computed on
   the UTC clock. The day anchor and numeric bucket widths are proven
   against the engine; the week, month, quarter, and year anchors are
   ported from the same arithmetic and left unproven.

The anchor modes and the calendar arithmetic are described on the
[Special indicators](special-indicators.md) page; the module below carries
the cumulative and daily forms.

## 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();
let deltaValue: f64 = NaN;

export function state(): i32 {
  const volume = in_volume();
  // Signed volume: the bar's volume counted toward the side of its close.
  deltaValue = cumDelta.update(in_close() >= in_open() ? volume : -volume);
  return isNaN(deltaValue) ? 0 : 1;
}
```

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
import { input, line, lower, ohlcv, output, overlay, param, time } from "./sdk/declare";
import { in_bar_t, in_close, in_high, in_low, in_open, in_volume } from "./gen/inputs";
import { emitRow, out_cum_delta, out_mfi, out_obv, out_vwap_cum, out_vwap_day } from "./gen/outputs";
import { p_mfi_period } from "./gen/params";
import { Cum, Mfi, Obv, Vwap } from "./sdk/ta";

param("mfi_period", 14, { min: 2, max: 200 });
input("close", ohlcv.close);
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("volume", ohlcv.volume);
input("bar_t", time.bar_open_sec);
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");
let mfiValue: f64 = NaN;
let obvValue: f64 = NaN;
let deltaValue: f64 = NaN;
let vwapCumValue: f64 = NaN;
let vwapDayValue: f64 = NaN;

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

export function state(): i32 {
  const close = in_close();
  const high = in_high();
  const low = in_low();
  const volume = in_volume();
  mfiValue = mfi.update(high, low, close, volume);
  obvValue = obv.update(close, volume);
  const open = in_open();
  // Signed volume: the bar's volume counted toward the side of its close.
  deltaValue = cumDelta.update(close >= open ? volume : -volume);
  const tsMs = in_bar_t() * 1000.0;
  vwapCumValue = vwapCum.update(open, high, low, close, volume, tsMs);
  vwapDayValue = vwapDay.update(open, high, low, close, volume, tsMs);
  return 1;
}

export function finalize(): void {
  out_vwap_cum(vwapCumValue);
  out_vwap_day(vwapDayValue);
  out_mfi(mfiValue);
  out_obv(obvValue);
  out_cum_delta(deltaValue);
  emitRow();
}

export function reset(): void {
  mfi.reset();
  obv.reset();
  cumDelta.reset();
  vwapCum.reset();
  vwapDay.reset();
  mfiValue = NaN;
  obvValue = NaN;
  deltaValue = NaN;
  vwapCumValue = NaN;
  vwapDayValue = NaN;
}
```

## 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 the port of kScript's `.buy` and `.sell` columns
is 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 lane serves `trades` volume
with a side; on your machine the same declarations fetch it.

```typescript
import { histogram, input, line, lower, none, ohlcv, output, trades } from "./sdk/declare";
import { in_buy, in_close, in_sell } from "./gen/inputs";
import { emitRow, out_cvd, out_delta, out_delta_sign } from "./gen/outputs";

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 delta: f64 = NaN;
let cvd: f64 = 0.0;

export function init(): void {}

export function state(): i32 {
  in_close();
  delta = in_buy() - in_sell();
  cvd += delta;
  return 1;
}

export function finalize(): void {
  out_delta(delta);
  out_delta_sign(delta >= 0.0 ? 1.0 : 0.0);
  out_cvd(cvd);
  emitRow();
}

export function reset(): void {
  delta = NaN;
  cvd = 0.0;
}
```

The primary input stays the close so the package follows the chart's
selector; the two side inputs align to its grid row for row. The
[aggregated CVD recipe](../cookbook/aggregated-cvd.md) extends this
across venues with `symbol` + `exchange` pins.

## 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; the same pattern on `mfi` above 80 is a classic exhaustion
  read.
- **Anchors.** The daily VWAP is the intraday fair price; the cumulative
  line is context that drifts with the loaded window.
