---
title: Aggregated CVD
description: >-
  One cumulative volume-delta line summed across four exchanges, with
  volatility bands and a live venue count. The multi-source recipe Pine can't
  easily write.
---

<div class="flex gap-3 mb-6">
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-purple-50 text-purple-600 text-sm font-medium">
    Advanced
  </span>
  <span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-gray-100 text-gray-600 text-sm font-medium">
    8 min read
  </span>
</div>

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.

```javascript title="scripts/probes/cookbook/aggregated_cvd.ks" lines wrap
//@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 `*Symbol` inputs from the settings panel to aggregate a different market. Mind each venue's naming: OKX swaps use the `BASE-QUOTE-SWAP` form while Binance and Bybit use the joined form. See [symbol format](/kscript/faq/symbol-format).
- **Add or drop venues.** More venues is just more `source(...)` lines plus more `pairDelta(...)` / `pairLive(...)` terms in the two sums, and bumping the `/ 4` in the table. To trade spot-only flow, delete the perp sources and keep the spot one.
- **Band width.** `bandLen` sets 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.** `posCol` and `negCol` drive the line, the bands, and the fill. The band and fill tints derive from them through `opacity()`, so recoloring stays consistent.

## Concepts used

- [Multi-source and aggregation](/kscript/core-concepts/multi-source) for combining venues in one script
- [Data sources](/kscript/core-concepts/data-sources) for the `buy_sell_volume` feed and its `.buy` / `.sell` members
- [User functions](/kscript/core-concepts/user-functions) for the `pairDelta` and `pairLive` na guards
- [Execution model](/kscript/core-concepts/execution-model) for `persist` state that accumulates across bars
- [Plotting](/kscript/functions/plotting) for the colored line, bands, fill, and table
