One cumulative volume-delta line with volatility bands and sign coloring, first on the chart's own market, then summed across four exchanges with a live venue count for your machine. The kScript (legacy) recipe and the Indicators it became, side by side.
Cumulative volume delta tracks the running difference between aggressive buying and aggressive selling. On a single venue it tells a partial story, because flow splits across exchanges. The kScript recipe summed the buy/sell delta from four venues at once (Binance spot, Binance futures, Bybit perps, OKX swaps) into one cumulative line, then framed it with volatility bands and a live table showing how many venues were reporting. The venue pins that make the sum possible are honored on your machine, so this page has two Indicators: the chart version reads its own market, and the aggregated variant keeps the four-venue sum and the live count.
The kScript (legacy) recipe
//@version=2
// ============================================================================
// AGGREGATED CVD
// kScript 3.0.10 source declarations require literal symbols or input-backed
// symbol strings. Keep venue symbols as inputs so the engine can fetch them.
// ============================================================================
define(title="Aggregated CVD (4 venues)", position="offchart", axis=true);
var bandLen = input(name="bandLen", type="number", defaultValue=60, label="Band Lookback", constraints={min: 10, max: 500, step: 10});
var posCol = input(name="posCol", type="color", defaultValue="#22d3a5", label="Positive");
var negCol = input(name="negCol", type="color", defaultValue="#ff5b7f", label="Negative");
var spotBSymbol = input(name="spotBSymbol", type="string", defaultValue="BTCUSDT", label="Binance Spot Symbol");
var perpBSymbol = input(name="perpBSymbol", type="string", defaultValue="BTCUSDT", label="Binance Futures Symbol");
var perpYSymbol = input(name="perpYSymbol", type="string", defaultValue="BTCUSDT", label="Bybit Perp Symbol");
var perpOSymbol = input(name="perpOSymbol", type="string", defaultValue="BTC-USDT-SWAP", label="OKX Swap Symbol");
// The chart's own series is the timeline spine. Aggregation scripts MUST load
// the main series: without it the bar timeline depends entirely on the venue
// sources resolving, and if any chart/coin lacks them the runtime gets zero
// bars to compute.
timeseries chart = ohlcv(symbol=currentSymbol, exchange=currentExchange);
timeseries spotB = source(type="buy_sell_volume", symbol=spotBSymbol, exchange="BINANCE");
timeseries perpB = source(type="buy_sell_volume", symbol=perpBSymbol, exchange="BINANCE_FUTURES");
timeseries perpY = source(type="buy_sell_volume", symbol=perpYSymbol, exchange="BYBIT");
timeseries perpO = source(type="buy_sell_volume", symbol=perpOSymbol, exchange="OKEX_SWAP");
// Per-venue na-guard: a venue with no data contributes zero instead of
// poisoning the sum (and the table below shows how many venues are live).
func pairDelta(b, s) {
if (isnum(b) && isnum(s)) { return b - s; }
return 0;
}
func pairLive(b, s) {
return (isnum(b) && isnum(s)) ? 1 : 0;
}
var delta = pairDelta(spotB.buy[0], spotB.sell[0]) + pairDelta(perpB.buy[0], perpB.sell[0])
+ pairDelta(perpY.buy[0], perpY.sell[0]) + pairDelta(perpO.buy[0], perpO.sell[0]);
var live = pairLive(spotB.buy[0], spotB.sell[0]) + pairLive(perpB.buy[0], perpB.sell[0])
+ pairLive(perpY.buy[0], perpY.sell[0]) + pairLive(perpO.buy[0], perpO.sell[0]);
persist cvd = 0;
cvd = cvd + delta;
timeseries cvdLine = cvd;
var vol = stddev(cvdLine, bandLen);
timeseries upper = cvd + (isnum(vol) ? vol : 0);
timeseries lower = cvd - (isnum(vol) ? vol : 0);
plotLine(cvdLine, colors=[posCol, negCol], colorIndex=cvd >= 0 ? 0 : 1, width=2, label=["Aggregated CVD"], desc=["Cumulative volume delta summed across four venues"]);
plotLine(upper, colors=[opacity(posCol, 35)], width=1, label=["Upper Band"], desc=["CVD plus one standard deviation"]);
plotLine(lower, colors=[opacity(negCol, 35)], width=1, label=["Lower Band"], desc=["CVD minus one standard deviation"]);
fillBetween(upper, lower, cvd >= 0 ? posCol : negCol, 0.12);
// Live diagnostics: if venues drop out you SEE it instead of a silent flat line.
if (isLastBar) {
plotTable(
data=[["Agg CVD", ""], ["Venues live", "".concat(live, " / 4")], ["CVD", "".concat(math.round(cvd))]],
position="top_right", headerRow=true, backgroundColor="#0d1117", textColor="#e6edf3", fontSize=11
);
}The Indicator (the chart's own market)
import { input, line, lower, none, ohlcv, output, param, trades } from "./sdk/declare";
import { in_buy, in_sell } from "./gen/inputs";
import { emitRow, out_cvd, out_lower_band, out_sign, out_upper_band } from "./gen/outputs";
import { p_band_len } from "./gen/params";
import { Stdev } from "./sdk/ta";
param("band_len", 60, { min: 10, max: 500, description: "Bars in the volatility band" });
// The chart's own candles are the grid every other input lines up on.
input("close", ohlcv.close);
input("buy", trades.volume, { side: "BUY", missing: "zero", description: "Aggressive buy volume" });
input("sell", trades.volume, { side: "SELL", missing: "zero", description: "Aggressive sell volume" });
output("cvd", line, lower, {
width: 2,
color_by: "sign",
colors: ["#ff5b7f", "#22d3a5"],
description: "Cumulative volume delta on the chart's market",
});
output("upper_band", line, lower, { color: "#22d3a5", opacity: 0.35, description: "CVD plus one standard deviation" });
output("lower_band", line, lower, { color: "#ff5b7f", opacity: 0.35, description: "CVD minus one standard deviation" });
output("sign", none, lower);
let stdev = new Stdev(60);
let cvd: f64 = 0.0;
let band: f64 = NaN;
export function init(): void {
stdev = new Stdev(i32(p_band_len()));
}
export function state(): i32 {
// A module-level variable is the accumulator: it survives from bar to bar until reset().
cvd += in_buy() - in_sell();
band = stdev.update(cvd);
return 1;
}
export function finalize(): void {
const width = isNaN(band) ? 0.0 : band;
out_cvd(cvd);
out_upper_band(cvd + width);
out_lower_band(cvd - width);
out_sign(cvd >= 0.0 ? 1.0 : 0.0);
emitRow();
}
export function reset(): void {
stdev.reset();
cvd = 0.0;
band = NaN;
}How it works
The timeline spine. input("close", ohlcv.close) is never read by the code. It is the first input, and the first input defines the grid every other input lines up on: one row per candle, always. Without it, the timeline would depend on the trade feed resolving, and on a market where it is missing the module would have no bars to compute. Load the chart series first, always, exactly as the kScript did.
Buy and sell volume. trades.volume with side: "BUY" and side: "SELL" are the two halves of the tape, the kScript's buy_sell_volume members as two inputs. missing: "zero" makes a bar with no trade observation contribute zero rather than carrying the last value forward, which would double-count.
Cumulative means module-level. cvd is a module-level variable and cvd += buy - sell adds each bar's net flow to the running total. That persistence is what turns a per-bar delta into a cumulative line, exactly what persist cvd = 0 did, and reset() is where it goes back to zero.
Sign coloring and the band. sign is a data-only 1 or 0, and the cvd line indexes its two-entry palette with it: green when the running total is positive, red when negative. Stdev over the running total gives the one-sigma envelope; while it is warming the band width is 0, so the edges sit on the line instead of vanishing.
What changed in the port
- Four
source(...)lines became one venue: on the chart every input reads the chart's own market, so venue pins are for your machine. The variant below keeps the four-venue sum and the live count. persist cvd = 0is a plainlet cvd: f64 = 0.0at module scope.colorIndex=iscolor_byover a data-only output;fillBetweenwould be a one-bar box between the two bands (the Anchored VWAP recipe shades its band that way).- The venues-live table became an output in the aggregated variant, readable on every bar instead of once on the last one.
- The symbol string inputs are gone: params are numbers, and a venue symbol is a pinned declaration.
The aggregated variant (four venues, for your machine)
Aggregated across four venues, for your machine (om wrun install, then om metric series or a watch; the chart reads its own market for every one of these inputs):
import { input, line, lower, none, ohlcv, output, param, trades } from "./sdk/declare";
import {
in_binance_buy,
in_binance_sell,
in_bybit_buy,
in_bybit_sell,
in_okx_buy,
in_okx_sell,
in_own_buy,
in_own_sell,
} from "./gen/inputs";
import { emitRow, out_cvd, out_lower_band, out_sign, out_upper_band, out_venues_live } from "./gen/outputs";
import { p_band_len } from "./gen/params";
import { Stdev } from "./sdk/ta";
param("band_len", 60, { min: 10, max: 500, description: "Bars in the volatility band" });
input("close", ohlcv.close);
// The chart's own venue follows the selector; the other three are pinned by symbol and exchange.
input("own_buy", trades.volume, { side: "BUY", missing: "nan" });
input("own_sell", trades.volume, { side: "SELL", missing: "nan" });
input("binance_buy", trades.volume, { side: "BUY", symbol: "BTCUSDT", exchange: "BINANCE", missing: "nan" });
input("binance_sell", trades.volume, { side: "SELL", symbol: "BTCUSDT", exchange: "BINANCE", missing: "nan" });
input("bybit_buy", trades.volume, { side: "BUY", symbol: "BTCUSDT", exchange: "BYBIT", missing: "nan" });
input("bybit_sell", trades.volume, { side: "SELL", symbol: "BTCUSDT", exchange: "BYBIT", missing: "nan" });
input("okx_buy", trades.volume, { side: "BUY", symbol: "BTC-USDT-SWAP", exchange: "OKEX_SWAP", missing: "nan" });
input("okx_sell", trades.volume, { side: "SELL", symbol: "BTC-USDT-SWAP", exchange: "OKEX_SWAP", missing: "nan" });
output("cvd", line, lower, { width: 2, color_by: "sign", colors: ["#ff5b7f", "#22d3a5"], description: "CVD summed across four venues" });
output("upper_band", line, lower, { color: "#22d3a5", opacity: 0.35 });
output("lower_band", line, lower, { color: "#ff5b7f", opacity: 0.35 });
output("venues_live", line, lower, { description: "Venues that reported this bar, 0 to 4" });
output("sign", none, lower);
let stdev = new Stdev(60);
let cvd: f64 = 0.0;
let band: f64 = NaN;
let live: f64 = 0.0;
// A venue with no observation this bar contributes zero, and does not count as live.
function pairDelta(buy: f64, sell: f64): f64 {
return isNaN(buy) || isNaN(sell) ? 0.0 : buy - sell;
}
function pairLive(buy: f64, sell: f64): f64 {
return isNaN(buy) || isNaN(sell) ? 0.0 : 1.0;
}
export function init(): void {
stdev = new Stdev(i32(p_band_len()));
}
export function state(): i32 {
cvd +=
pairDelta(in_own_buy(), in_own_sell()) +
pairDelta(in_binance_buy(), in_binance_sell()) +
pairDelta(in_bybit_buy(), in_bybit_sell()) +
pairDelta(in_okx_buy(), in_okx_sell());
live =
pairLive(in_own_buy(), in_own_sell()) +
pairLive(in_binance_buy(), in_binance_sell()) +
pairLive(in_bybit_buy(), in_bybit_sell()) +
pairLive(in_okx_buy(), in_okx_sell());
band = stdev.update(cvd);
return 1;
}
export function finalize(): void {
const width = isNaN(band) ? 0.0 : band;
out_cvd(cvd);
out_upper_band(cvd + width);
out_lower_band(cvd - width);
out_venues_live(live);
out_sign(cvd >= 0.0 ? 1.0 : 0.0);
emitRow();
}
export function reset(): void {
stdev.reset();
cvd = 0.0;
band = NaN;
live = 0.0;
}Four venues, one file. Each pinned venue names its own symbol (OKX swaps use the BASE-QUOTE-SWAP form, Binance and Bybit the joined form) and pins symbol and exchange together, the pair rule. There is no special "aggregate" primitive, just eight inputs and addition: combining venues is the same as combining any two inputs (Multi-source).
Missing venues contribute zero, not chaos. missing: "nan" instead of "zero" is what makes pairLive possible: a venue with no observation reads NaN, counts as not live, and contributes zero through pairDelta. So the aggregate degrades gracefully: three live venues still produce a clean line, and venues_live says it is three, not four. The unpinned pair follows whatever market the package is evaluated on, so on a Binance futures market the four venues are the kScript's four.
Install it and read the sum against the futures market it follows:
om wrun install ./aggregated-cvd --replace
om metric series --metric wrun/@you/aggregated-cvd/cvd --symbol BTCUSDT --exchange BINANCE_FUTURES --bars 120
om metric get --metric wrun/@you/aggregated-cvd/venues_live --symbol BTCUSDT --exchange BINANCE_FUTURESCustomize it
- Swap the coin. The pinned symbols are declarations, so edit the four strings and build again; the unpinned pair follows the market you evaluate on. Mind each venue's naming (Exchange and symbol format).
- Add or drop venues. More venues are two more inputs and one more term in each sum. Spot-only flow is the spot inputs alone.
- Band width.
band_lensets the volatility lookback. Shorten it for a reactive envelope that hugs the line, lengthen it for a smoother, slower band. - Spot vs perp split. Keep two accumulators and two lines; divergence between them, perps buying while spot sells, is often the interesting signal.
- Colors. The palette on
cvdand thecoloron each band are hex strings on the declarations; recolor there.
Concepts used
- Multi-source for pinned inputs and arithmetic across venues
- Data sources for
trades.volumewith asideand themissingpolicies - User functions for the
pairDeltaandpairLivehelpers - Execution model for the module-level accumulator and where pins are honored
- Volume indicators for buy and sell volume as inputs