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. This recipe sums the buy/sell delta from four venues at once (Binance spot, Binance futures, Bybit perps, OKX swaps) into one cumulative line, then frames it with volatility bands and a live table that shows how many venues are actually reporting. This is the kind of script kScript makes natural and most charting languages make painful: every venue is just another source in the same script.
//@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, 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
);
}How it works
The timeline spine. The first source is the chart's own ohlcv. It looks redundant since the script never plots it, but it defines the bar sequence every other source aligns to. Without it, the timeline would depend entirely on the venue feeds resolving, and on a symbol where one is missing the script would have no bars to compute. Load the chart series first, always.
Four venues, one script. Each source(type="buy_sell_volume", ...) subscribes to a different exchange's aggressive buy and sell volume. Binance spot, Binance futures, Bybit, and OKX each become an ordinary series with .buy and .sell members. There is no special "aggregate" primitive here, just four sources and addition. That is the multi-source superpower: combining venues is the same as combining any two series.
Each venue's symbol is an input. The engine resolves source symbols at compile time, so they have to be literals or input-backed strings. Exposing them as inputs (spotBSymbol and friends) is what lets you point the whole aggregate at a different coin from the settings panel without editing code. The OKX default uses that venue's dash-style symbol (BTC-USDT-SWAP), a reminder that venues name the same market differently.
Missing venues contribute zero, not chaos. A venue can be late to list a coin or briefly drop out. pairDelta returns 0 when either side is na instead of poisoning the sum, and pairLive returns 1 only when a venue is actually reporting. So the aggregate degrades gracefully: three live venues still produce a clean line, and the table tells you it is three, not four.
Cumulative means persistent. persist cvd = 0 declares one accumulator that survives across bars, and cvd = cvd + delta adds each bar's net flow to the running total. That persistence is what turns a per-bar delta into a cumulative line. The line is colored green when the running total is positive and red when negative via colorIndex.
The bands and the diagnostics. stddev over the cumulative line gives a one-sigma envelope, plotted faint above and below and shaded with fillBetween, so you can read momentum against its own recent volatility. On the last bar a small plotTable reports how many venues are live and the rounded CVD. If a feed drops, you see "3 / 4" instead of silently trusting a line that lost a quarter of its input.
Customize it
- Swap the coin. Change all four
*Symbolinputs from the settings panel to aggregate a different market. Mind each venue's naming: OKX swaps use theBASE-QUOTE-SWAPform while Binance and Bybit use the joined form. See symbol format. - Add or drop venues. More venues is just more
source(...)lines plus morepairDelta(...)/pairLive(...)terms in the two sums, and bumping the/ 4in the table. To trade spot-only flow, delete the perp sources and keep the spot one. - Band width.
bandLensets the volatility lookback. Shorten it for a reactive envelope that hugs the line, lengthen it for a smoother, slower band. - Spot vs perp split. Instead of one combined line, keep two accumulators (spot delta and perp delta) and plot both. Divergence between them, perps buying while spot sells, is often the interesting signal.
- Colors.
posColandnegColdrive the line, the bands, and the fill. The band and fill tints derive from them throughopacity(), so recoloring stays consistent.
Concepts used
- Multi-source and aggregation for combining venues in one script
- Data sources for the
buy_sell_volumefeed and its.buy/.sellmembers - User functions for the
pairDeltaandpairLivena guards - Execution model for
persiststate that accumulates across bars - Plotting for the colored line, bands, fill, and table