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 page; for per-price buy and sell
volume inside one bar see Volume profile.
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:
- 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.
- 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
NaNuntil the next bucket starts (forever, in the cumulative form), and a zero total volume givesNaN. - 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 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).
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).
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.
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.
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 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.
deltaandcvdsay which side did the volume, not just how much. - Divergence. Price at a new high while
obvorcvdis not is a warning; the same pattern onmfiabove 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.