---
title: Whale vs Retail CVD
description: >-
  Separate whale flow from retail flow using the tape pre-bucketed into real
  USD order-size bands, with months of history and per-bar trade counts.
verified:
  engine: 3.0.67
  probes:
    - scripts/probes/tape/tape_tvbs_whale.ks
    - scripts/probes/tape/tape_htf.ks
---

<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">
    5 min read
  </span>
</div>

Candles tell you where price went; the tape tells you who pushed it. The `trade_volume_by_size` source hands your script every bar's tape pre-bucketed into fixed USD order-size bands, one cell per populated bucket: `[bucketId, buyVolUsd, sellVolUsd, buyCount, sellCount]`. That makes separating whale flow from retail flow a one-line reduction, with months of history behind it.

## Whale vs retail flow in USD

The bucket source classifies every trade by USD notional into fixed bands (1 = $0-1K up to 7 = $10M+), so `c[0] >= 5` means "half a million dollars and up, per trade":

```javascript title="Whale vs Retail CVD" lines wrap
//@version=2
define(title="Whale vs Retail CVD", position="offchart", axis=true);

timeseries ohlcvData = ohlcv(symbol=currentSymbol, exchange=currentExchange);
timeseries tv = trade_volume_by_size(symbol=currentSymbol, exchange=currentExchange);

var cells = tv.cells;
var whaleFlow = cells.filter((c) => c[0] >= 5).map((c) => c[1] - c[2]).reduce((a, b) => a + b, 0);
var retailFlow = cells.filter((c) => c[0] <= 2).map((c) => c[1] - c[2]).reduce((a, b) => a + b, 0);
var barTrades = cells.map((c) => c[3] + c[4]).reduce((a, b) => a + b, 0);

static whaleCvd = 0
static retailCvd = 0
whaleCvd = whaleCvd + whaleFlow
retailCvd = retailCvd + retailFlow

plotLine(value=whaleCvd, colors=["#ab47bc"], width=2, label=["Whale CVD USD"], desc=["CVD from 500K+ USD order-size buckets"]);
plotLine(value=retailCvd, colors=["#26c6da"], width=2, label=["Retail CVD USD"], desc=["CVD from sub-10K USD order-size buckets"]);
plotHistogram(value=barTrades, colors=["#ff9100"], width=0.8, label=["Trades"], desc=["Total trades per bar across all buckets"]);
```

The divergence between the purple and cyan lines is the signal: whales accumulating into retail selling, or distribution into retail chasing. `barTrades` sums the per-bucket counts, so the histogram is the honest per-bar trade count without touching individual prints.

## The rules of the road

- **Values are USD notional** (tens of millions on liquid perps). Keep one unit system per script: mixing these series with base-unit volume in one pane flattens the smaller series into an unreadable line at the bottom.
- **Data depth is layered.** USD buckets: about a week at 1m, months at 1h and 1d. `buy_sell_volume`: years (add `currency="USD"` for notional). Long-range CVD belongs to `buy_sell_volume`, whale history to the buckets.
- **Multi-timeframe goes through `htf()`** (cells concatenate across the bucket); `requestBars()` and `ltf()` reject array-celled sources.
- **Live budget:** more than a handful of concurrently polling tape scripts per account renders the excess as static tape with a `TAPE_POLL_BUDGET_EXCEEDED` notice.
