Where does Hyperliquid CVD data come from?
There is no native hyperliquid cvd data. Cumulative volume delta, the running sum of taker buy volume minus taker sell volume, is not a field Hyperliquid publishes for any coin on any interval. What the venue publishes is a public trades stream in which each fill carries a side: B when the taker bought, A when the taker sold. Everything called CVD, taker ratio or aggressor flow on Hyperliquid is someone adding those fills up.
That has two consequences worth stating up front. First, two vendors' CVD lines for the same coin will differ wherever their stream coverage differed, because a dropped socket is a hole in the sum and the venue offers no replay to fill it. Second, the sum has to start somewhere, so an absolute CVD number means nothing on its own; only its slope over a window you chose does.
Our implementation is a collector that holds the public trades socket for the whole perp universe and closes a bucket per coin per UTC minute. It stores 1-minute buckets, not per-fill rows: the taker buy and sell notional, the trade count, a VWAP, the largest fill and the share of notional from large fills. Per-fill data for Hyperliquid exists in our archive only for liquidations, which come from a different source (node fill data) and are covered in a separate post.
What /hyperliquid/trade-flow returns
GET /api/v1/hyperliquid/trade-flow?coin=BTC&minutes=60 returns the trailing minutes closed minutes for one perp (1 to 1440), newest last and dense, meaning one row per minute whether or not anything traded. Pro, Pro Plus included.
curl "https://cryptodataapi.com/api/v1/hyperliquid/trade-flow?coin=BTC&minutes=60" \
-H "X-API-Key: cdk_live_your_key"
Abridged response, illustrative values, served field names:
{
"coin": "BTC", "minutes": 60, "bucket": "1m", "large_fill_usd": 25000,
"window_start": 1792970400000, "window_end": 1792974000000,
"stream": { "connected": true, "connected_since": 1792911034120, "coverage_pct": 100.0 },
"data": [
{ "time": 1792970400000, "coin": "BTC", "buy_notional": 1842310.2, "sell_notional": 1210044.7,
"n_trades": 412, "vwap": 68412.5, "max_fill_usd": 310200.0, "large_fill_share": 0.31,
"partial": false, "cvd_usd": 632265.5 },
...
],
"count": 60
}
Row fields, with their exact meaning:
time: minute start, ms.window_endis exclusive and the still-open minute is never included.buy_notional/sell_notional: USD by aggressor side. Their sum is the minute's traded notional.n_trades,vwap(notional-weighted;nullif no fills),max_fill_usd.large_fill_share: the 0 to 1 share of notional from fills of at leastlarge_fill_usd, which is $25k;nullwhen no fill qualifies.partial:truemeans our socket did not watch the whole minute.cvd_usd: runningbuy_notional - sell_notionalfromwindow_start. It resets per request, so compare rows within one response only. It is served, not archived.
The envelope's stream.coverage_pct is the share of the last 60 minutes that was fully watched. Until the first bucket closes after a cold start the endpoint answers 503 trade_flow_warming with Retry-After: 60; a coin Hyperliquid does not list is a 404 unknown_coin.
Null vs zero vs a missing minute
Because the sum is only as good as the socket, the endpoint is explicit about what it did not see. This table is the part to lift into your parser:
| You see | It means | Do |
|---|---|---|
buy_notional: 0.0, partial: false | We watched the whole minute and nothing traded | Count it as a real zero |
buy_notional: null, partial: true | A reconnect or deploy; the minute was not fully watched | Break your CVD series there; never interpolate |
vwap: null | No fills in the minute | Carry the previous VWAP if you need a price |
large_fill_share: null | No fill reached $25k | Treat as 0 for a filter, null for a mean |
A minute absent from /backtesting/hl-trade-flow | Either quiet or unwatched; stored partial rows tell them apart | Check for a stored partial row before calling it quiet |
Unwatched minutes are never back-filled. Hyperliquid's stream has no replay, so a hole at capture time is a hole forever, and the honest thing is to say so in the row rather than smooth it.
Scanning every perp at once: /trade-flow/universe
GET /api/v1/hyperliquid/trade-flow/universe?window=1h (Pro Plus) aggregates the same buckets across every Hyperliquid perp for one window of 15m, 1h or 4h, biggest notional first. Each row carries coin, buy_notional, sell_notional, taker_buy_ratio (buy divided by buy plus sell, 0 to 1; null if no fills), n_trades, large_fill_share and partial_minutes. The envelope has window, window_minutes, large_fill_usd, as_of (the window's end, the last closed minute) and count.
curl "https://cryptodataapi.com/api/v1/hyperliquid/trade-flow/universe?window=1h" \
-H "X-API-Key: cdk_live_your_key"
partial_minutes is the same number on every row, because coverage is a property of the connection, not the coin. Coins with zero fills in the window are omitted rather than reported as zeros. Warming and coverage semantics match the single-coin route.
The cross-venue read: /market-intelligence/taker-buy-sell
If you want Hyperliquid's taker bias next to other exchanges, GET /api/v1/market-intelligence/taker-buy-sell reports a taker buy ratio per exchange, per coin, over a 4-hour window. Here buy_ratio is a percent, 0 to 100, not a fraction. Since 2026-09-09 every coin Hyperliquid lists has an {"exchange": "Hyperliquid", "buy_ratio": ...} leg in the inner exchange_list, notional-weighted over the trailing 4h from the same trade tape; a coin only Hyperliquid carries gets its own top-level entry whose aggregate is the Hyperliquid figure. Any key can read it.
Two scales for the same idea is the kind of thing that costs an afternoon, so:
| Endpoint | Field | Scale | Window |
|---|---|---|---|
/hyperliquid/trade-flow/universe | taker_buy_ratio | 0 to 1 | 15m, 1h or 4h, your choice |
/market-intelligence/taker-buy-sell | buy_ratio | 0 to 100 | 4h, fixed |
An absent Hyperliquid leg means the tape is warming or not configured, never that Hyperliquid had no trades.
How do I get historical Hyperliquid taker volume to rebuild CVD?
From GET /api/v1/backtesting/hl-trade-flow (Pro), the archived minutes, and from the hl_trade_flow daily Parquet type for bulk. Coverage begins 2026-09-09, the day the collector went live; it is forward-only and nothing earlier can be reconstructed, for the same no-replay reason. Rows have the live shape minus cvd_usd: time, coin, buy_notional, sell_notional, n_trades, vwap, max_fill_usd, large_fill_share, partial. Minutes with no fills are not stored. Pass coin (or a comma list up to 25, or omit for all), a required start, an exclusive end, and page with cursor; format=csv works too.
curl "https://cryptodataapi.com/api/v1/backtesting/hl-trade-flow?coin=ETH&start=2026-10-01&end=2026-10-02&limit=10000" \
-H "X-API-Key: cdk_live_your_key"
Rebuilding CVD from the archive is a cumulative sum that resets at every unwatched minute:
import httpx
API = "https://cryptodataapi.com/api/v1"
H = {"X-API-Key": "cdk_live_your_key", "Accept-Encoding": "gzip"}
rows, cursor = [], None
while True:
p = {"coin": "ETH", "start": "2026-10-01", "end": "2026-10-02", "limit": 10000}
if cursor: p["cursor"] = cursor
j = httpx.get(f"{API}/backtesting/hl-trade-flow", headers=H, params=p, timeout=60).json()
rows += j["data"]
if not j.get("has_more"): break
cursor = j["next_cursor"]
cvd, series = 0.0, []
for r in rows:
if r["partial"]: # unwatched minute: the sum is no longer continuous
cvd = 0.0
series.append((r["time"], None))
continue
cvd += r["buy_notional"] - r["sell_notional"]
series.append((r["time"], cvd))
For whole days across all coins, list and download the Parquet files with /backtesting/archives?data_type=hl_trade_flow and /backtesting/archives/download (Pro Plus), or buy a single day-file for $1.00 over x402 at /backtesting/archives/purchase. Columns match the row above.
How to use this in a bot (and when not to)
Use the slope, not the level. Because cvd_usd restarts at window_start on every request, compare the last N rows of one response, or compute your own running sum from buy_notional and sell_notional over a window you control.
Gate on coverage. If stream.coverage_pct is well under 100, or your window contains partial: true rows, the taker ratio for that window is biased by what was missed. Skip the signal rather than act on it.
Pair flow with price. Taker buying into a flat or falling vwap is absorption; the same buying with price rising is confirmation. The minute grid lines up with /hyperliquid/candles?interval=1m on bar open, so the join is a dictionary lookup on time.
Do not read liquidations out of this tape. The public trades stream carries no liquidation flag, so a forced close looks like any other aggressive fill here. The exact liquidation fills are a separate feed at /backtesting/hl-liquidations. Do not expect per-fill rows; the collector stores minutes. And do not expect anything before 2026-09-09.
The single-coin route is on Pro at $29 a month and the 24-hour Pro trial on email verification reaches it; the universe scan is Pro Plus. Reference at /docs, plans at /pricing.
Frequently asked questions
Does Hyperliquid publish CVD or taker buy/sell volume?
No. CVD is rebuilt from the public trades stream, where each fill's side says whether the taker bought (B) or sold (A). Every Hyperliquid CVD figure is somebody's sum of those fills.
What does cvd_usd on /hyperliquid/trade-flow measure?
The running buy_notional minus sell_notional from window_start of that request. It resets per request and is not archived, so compare rows within one response or compute your own sum from the archived minutes.
What does a null buy_notional with partial true mean?
Our socket did not watch the whole minute, typically a reconnect or deploy. Hyperliquid's stream has no replay, so the minute is never back-filled. A watched minute with no trades is a real zero with partial false.
How far back does Hyperliquid taker flow history go?
To 2026-09-09, when the collector went live. /backtesting/hl-trade-flow serves the archived 1-minute buckets and the hl_trade_flow Parquet type holds one file per day for all coins.



