Backtest on Hyperliquid whale and smart money positions — hour by hour, as they were then
Who was long and who was short on Hyperliquid, every hour: by account size (whales to shrimp) and by realized PnL (smart money to rekt), each hour graded with the label the wallet had then, not today. Cohort data from 7 April 2026, all of it in one request; the Parquet archive adds every 5-minute position snapshot with its labels. Free, over MCP, REST and the CLI.
“Does Hyperliquid's Smart Money Index lead BTC? Use the last 90 days of hourly readings.”
hl_market hl_ohlcv“What did BTC do in the 6 hours after the 3x rekt cohort was most long? Use point-in-time labels, last 90 days.”
hl_cohort_metrics hl_ohlcv“Give me the last 30 days of hourly long/short positioning for ETH by account size, as a table I can backtest on.”
hl_cohort_metricsWhy “as they were then” matters: today's labels change the answer
A backtest asks: when this cohort was long, what did the price do next? The answer depends on WHO was in the cohort at that hour. The realized-PnL ladder re-grades every wallet as it trades, so a wallet graded 3x rekt today may have been graded winner a month ago. By default a pnl cohort is today's grading applied to every past hour; that lets a backtest know which wallets went on to win or lose, which quants call look-ahead bias. labelBasis=point_in_time gives each hour the label the wallet had at that hour.
Measured on BTC axis=pnl for the same 720 settled hours (2026-10-05 07:00 to 2026-11-04 06:00 UTC), once with each hour's own labels and once with today's: the share of hours in which the cohort sits on the other side (net long under one labelling, net short under the other), and how far the two bias lines are apart on average (bias runs from -1, all short, to +1, all long).
| Cohort (BTC, axis=pnl) | Hours on the other side | Of | Average gap between the two bias lines |
|---|---|---|---|
3x rekt | 32.2% (232) | 720 | 0.2536 |
winner | 29.2% (210) | 720 | 0.2556 |
2x winner | 15.8% (114) | 720 | 0.1431 |
3x loser | 13.1% (94) | 720 | 0.1976 |
2x loser | 9.7% (70) | 720 | 0.1063 |
2x smart | 5.3% (38) | 720 | 0.0688 |
smart | 0.1% (1) | 720 | 0.0568 |
3x smart | 0.0% (0) | 720 | 0.1162 |
3x winner | 0.0% (0) | 720 | 0.0623 |
loser | 0.0% (0) | 720 | 0.0648 |
rekt | 0.0% (0) | 720 | 0.064 |
2x rekt | 0.0% (0) | 720 | 0.0304 |
unknown | 0.0% (0) | 720 | 0.0485 |
3x rekt sat on the other side in 32.2% of hours (232 of 720).
winner sat on the other side in 29.2% of hours (210 of 720).
2x winner sat on the other side in 15.8% of hours (114 of 720).
3x loser sat on the other side in 13.1% of hours (94 of 720).
3x smart sat on the other side in 0.0% of hours (0 of 720).
Ask for each hour's own labels
import os, requests
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/cohort-metrics",
params={"axis": "pnl", "labelBasis": "point_in_time", "hours": 720}, # 30 days, each hour labelled as it was
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
timeout=60,
)
m = r.json()
assert m["label_computed"]["lookahead"] is False # the response says how its labels were made: check it
print("cohort data from", m["data_from"]) # the first hour the labels are correct: nothing earlier is served
for row in m["metrics"][:13]: # the newest hour, one row per cohort
print(row["ts"], row["cohort"], row["bias"], row["label_basis"])
# Claude, ChatGPT, Cursor: ask in words; or from the terminal:
vike-data call hl_cohort_metrics --args '{"symbol": "BTC", "axis": "pnl", "label_basis": "point_in_time", "hours": 720}'
vike-data hl cohort-metrics BTC --axis pnl --label-basis point_in_time --hours 720
What there is for a backtest
| Series | Where | From | Grain | One request | Labels |
|---|---|---|---|---|---|
| Long/short by account size or position tier, one coin | /symbols/{symbol}/cohort-metrics?axis=size | 2026-04-07 | hourly, every cohort | all of it (up to 8,760 hourly buckets: hours) | as they were: size_cohort is stored on the row when it is written |
| Long/short by realized PnL, one coin | /symbols/{symbol}/cohort-metrics?axis=pnl&labelBasis=point_in_time | 2026-04-07 | hourly, every cohort | all of it (up to 8,760 hourly buckets: hours) | as they were with labelBasis=point_in_time; daily before 2026-08-06, hourly from it |
| Smart Money Index, Whale-vs-Shrimp Index, one coin | /symbols/{symbol}/indices | 2026-04-07 | hourly | 5,000 hours a page, has_more says there are older ones | series as they were; latest is today's |
| One cohort across every coin | /segments/{cohort}/history?axis=size|pnl | 2026-04-07 | hourly | 5,000 hours a page, has_more | as they were (no labelBasis to pass) |
| Every open position, every 5 minutes (Parquet) | /v1/hyperliquid/archive/url?dataset=positions | 2026-05-08 | 5-minute snapshots, one file a UTC day | one day: 116–123 million rows, 1.6–1.9 GB a day | size_cohort, pnl_cohort, account_value on every row, as of the start of the row's hour |
| Every fill (Parquet) | /v1/hyperliquid/archive/url?dataset=fills | 2026-05-01 | every fill, one file a UTC day | one day: 6.8–20.9 million rows a day | none: join a fill to the positions file's labels on wallet and hour, as the large-fills backtest does |
| Price: candles | /v1/hyperliquid/ohlcv | the end of 2025-12-31 | 1 minute to 1 day, and tick bars | 5,000 bars | — |
Archive sizes: the archive index on 2026-11-04, its seven newest days. Fills, candles and every archive column: Hyperliquid historical data.
Labels as they were, or as they are today
Every default and refusal below is the API's own: its validators run when this page is built.
A cohort label is a fact about a wallet at a moment. labelBasis picks the moment: point_in_time is the label the wallet had in that row's own hour, current is today's, resolved when the request runs. grading picks the PnL ladder. The two are independent, which is why every answer says in label_computed when its labels were made.
labelBasis, axis by axis
| axis | Default | Ask for the other | The API answers |
|---|---|---|---|
size | point_in_time | labelBasis=current | 400: axis='size' is always 'point_in_time': size_cohort is stored on the row at ingestion, so it is already point-in-time. Drop labelBasis, or use axis=pnl for a frozen ladder. |
tier | point_in_time | labelBasis=current | 400: axis='tier' is always 'point_in_time': the tier bucket is derived from size_usd, which is stored on the row at ingestion, so it is already point-in-time. Drop labelBasis, or use axis=pnl for a frozen ladder. |
pnl | current | labelBasis=point_in_time | accepted |
pnl_abs | current | labelBasis=point_in_time | 400: axis='pnl_abs' is always 'current': pnl_abs is banded from the dictionary's current cumulative realized PnL and was never frozen anywhere, so a point-in-time answer does not exist. Drop labelBasis, or use axis=pnl for a frozen ladder. |
pnl_unrealized | point_in_time | labelBasis=current | 400: axis='pnl_unrealized' is always 'point_in_time': the unrealized grading is hour H's rank over the wallet's last snapshot strictly before H, computed by the hourly rollup and stored; a current-graded variant would re-rank history with today's marks — the lookahead the realized axis was rebuilt on 2026-08-17 to remove. Drop labelBasis, or use axis=pnl for a frozen ladder. |
grading: which PnL ladder
| grading | labelBasis | label_computed: when | lookahead | resolution |
|---|---|---|---|---|
realized the default on axis=pnl | current | at query time | true | none — one label per wallet, today's |
realized use it for every backtest | point_in_time | before 2026-08-06, reconstructed from fills on 2026-08-17; from 2026-08-06, frozen on the position row at its own hour | false | daily before 2026-08-06, hourly after |
realized-pit a control, rebuilt daily | point_in_time | reconstructed from fills, day D graded on the cumulative as of the end of D-1 | false | daily |
unrealized positioning, not skill | point_in_time | at hour H, over the wallet's last snapshot strictly before H | false | hourly |
Refused, by name
/cohort-metrics?axis=size&labelBasis=current→ 400: axis='size' is always 'point_in_time': size_cohort is stored on the row at ingestion, so it is already point-in-time. Drop labelBasis, or use axis=pnl for a frozen ladder./cohort-metrics?axis=pnl&grading=realized-pit&labelBasis=current→ 400: grading='realized-pit' is always 'point_in_time': it IS the point-in-time rebuild of the realized ladder, so a current-graded variant of it is exactly grading='realized' with labelBasis=current. Drop labelBasis, or use grading='realized' for a query-time ladder./cohort-metrics?axis=pnl&grading=unrealized&labelBasis=current→ 400: grading='unrealized' is always 'point_in_time': the unrealized grading is hour H's rank over the wallet's last snapshot strictly before H, computed by the hourly rollup and stored; a current-graded variant would re-rank history with today's marks. Drop labelBasis, or use grading='realized' for a query-time ladder./symbols/{symbol}/metrics?axis=pnl&labelBasis=point_in_time→ 400: this endpoint cannot serve labelBasis=point_in_time: its cohort label is resolved from the wallet dictionary at QUERY time, so every historical hour it returns carries today's grading (measured 2026-04-20T12:00Z on BTC's '3x smart': 8.91% more notional than the point-in-time labelling of the same hour). Use /v1/hl-cohorts/symbols/{symbol}/cohort-metrics with axis=pnl&labelBasis=point_in_time, which reads the frozen ladder. ⚠ Before 2026-08-19 this parameter was not declared here and FastAPI silently ignored it, so the same request returned 200 with lookahead rows.
What every answer declares
data_from: the first hour the cohort labels are correct; nothing earlier is served (an olderstartis clamped to it)label_basis: on the envelope AND on every row:point_in_timeorcurrent, so rows concatenated from two calls still say how each was labelledgrading: which PnL ladder labelled the rows (realized,realized-pit,unrealized), null on axes that rank no PnLlabel_computed: WHEN the label was computed:when,lookahead(true or false),resolution,source,detail, and acaveatwhere one applies; on size and tier there is no choice, so no blockfreshness: for a window that reaches now:newest_ts,behind_hours,staleunavailable_fields: with labelBasis=point_in_time over a window before 2026-08-07: the five position fields that come back null, never 0
The label_computed block of axis=pnl&labelBasis=point_in_time, as the API returns it:
{
"grading": "realized",
"label_basis": "point_in_time",
"when": "before 2026-08-06, reconstructed from fills on 2026-08-17; from 2026-08-06, frozen on the position row at its own hour",
"lookahead": false,
"resolution": "daily before 2026-08-06, hourly after",
"source": "hourly cohort snapshots",
"detail": "Two point-in-time definitions meeting at 2026-08-06. Before it: day D's label is the wallet's rank by cumulative closed PnL as of the END of D-1, reconstructed from fills by the 2026-08-17 rebuild \u2014 so a label is constant across D's 24 hours. From it: the hourly rollup stamps the dictionary value as of that hour onto the row, so the label moves within a day. Neither half can see the future, but a series crossing the seam changes its label RESOLUTION, which a difference or a turnover count will read as a regime change that is an artefact.",
"caveat": "Rows before 2026-08-06 carry no label frozen at write time: their label is the daily reconstruction described above, not a value stamped on the row at its own hour. From 2026-08-06 the label is frozen on the row and does not change."
}
Routes that state their basis instead of taking one
/v1/hl-cohorts/segments/{cohort}/history: takes no labelBasis: as they were on both axes;label_computedon pnl/v1/hl-cohorts/symbols/{symbol}/indices:seriesas they were;latestis the newest 5-minute snapshot graded today (latest.label_basis: current): keep it out of a backtest/v1/hl-cohorts/symbols/{symbol}/large-fills:pnl_cohortat the fill's own hour where that hour is labelled, else today's, andpnl_cohort_basissays which on every row;size_cohortis today's (size_cohort_basis: current)/v1/hyperliquid/archive/url?dataset=positions:pnl_cohort,size_cohortandaccount_valueon every row, as of the start of the row's hour: no later information/v1/hyperliquid/positions: a historical row'spnl_cohortis its own clock hour's, which counts that hour's later fills (label_computed.lookahead: true); every row sayspnl_cohort_basisandpnl_cohort_as_of.label_causalon the same row holdspnl_cohort,pnl_abs_cohort,pnl_cohort_as_ofandpnl_cohort_basisas of the hour before, the archive's label (label_computed.causal_field,causal_lookahead: false), null where that hour has no label: use it in a backtest/v1/hyperliquid/hype/holders:pnl_cohortis today's (label_basis: current), and rightly: the holder list has no time axis for a current label to contaminate
Ready backtests, each with its result first
Every code block ran against the live API on 2026-11-04; each result is that run's own output. A weak result is shown as it came out.
Did cohort positioning predict price? A backtest on labels as they were
Compare the smart cohort's hourly BTC bias with the next hour's return over 30 days, using point-in-time labels so the cohort is the one that existed that hour; then run the same test with today's labels and see the answer change.
- Uses
/symbols/{…}/cohort-metrics,/v1/hyperliquid/ohlcv- Verified
- live API, 2026-11-04: 5 calls, all 200
3x rekt was net long in 421 hours and its bias correlated +0.076 with the next hour's BTC return; with today's labels it was net long in all 721 and the correlation flipped to -0.069. The first script, on 3x smart, finds no edge: that cohort was net short every hour, correlation -0.054.The code
"""Does the smart cohort's BTC positioning say anything about the next hour's return? A point-in-time check.
Run: VIKE_API_KEY=... python hl-cohort-backtest.py (--dry-run lists the requests and makes none)
"""
import os
import sys
from datetime import datetime
from statistics import correlation, mean
from urllib.parse import urlencode
NAME = "hl-cohort-backtest"
BASE = "https://data.vike.io"
DRY = "--dry-run" in sys.argv
COHORT = "3x smart"
HOUR_MS = 3_600_000
def fail(why):
sys.exit(f"{NAME}: {why}")
def call(path, method="GET", **params):
url = BASE + path + ("?" + urlencode(params) if params else "")
if DRY:
print(method, url)
return None
key = os.environ.get("VIKE_API_KEY") or fail("set VIKE_API_KEY")
import requests
r = requests.request(method, url, headers={"X-API-Key": key}, timeout=60)
if r.status_code != 200:
fail(f"{method} {path} answered HTTP {r.status_code}: {r.text[:200]}")
return r.json()
def main():
# labelBasis=point_in_time: each row carries the label the wallet had THAT hour. The default
# resolves labels at query time, which hands a backtest today's grading (lookahead).
matrix = call(
"/v1/hl-cohorts/symbols/BTC/cohort-metrics",
axis="pnl", labelBasis="point_in_time", hours=800,
start="2026-10-01T00:00:00Z", end="2026-10-31T00:00:00Z",
)
candles = call(
"/v1/hyperliquid/ohlcv",
symbol="BTC", interval="1h", limit=800,
start="2026-10-01T00:00:00Z", end="2026-10-31T02:00:00Z",
)
if DRY:
return
if matrix["label_basis"] != "point_in_time":
fail("labels are not point-in-time; a backtest on them would look ahead")
ret = {c["ts"]: c["close"] / c["open"] - 1 for c in candles["candles"]}
pairs = [] # (bias in hour H, return of hour H+1): positioning is read before the move it is compared to
for m in matrix["metrics"]:
if m["cohort"] != COHORT or m["bias"] is None:
continue
hour_ms = int(datetime.fromisoformat(m["ts"]).timestamp() * 1000)
if hour_ms + HOUR_MS in ret:
pairs.append((m["bias"], ret[hour_ms + HOUR_MS]))
if len(pairs) < 10:
fail(f"only {len(pairs)} usable hours for cohort {COHORT!r}")
long_side = [r for b, r in pairs if b > 0]
short_side = [r for b, r in pairs if b <= 0]
print(f"{COHORT}, BTC, {len(pairs)} hours (labels: point_in_time)")
print(f" corr(bias, next-hour return) = {correlation([b for b, _ in pairs], [r for _, r in pairs]):+.3f}")
if long_side:
print(f" next-hour return after net-long hours : {mean(long_side) * 1e4:+.1f} bp over {len(long_side)} hours")
if short_side:
print(f" next-hour return after net-short hours: {mean(short_side) * 1e4:+.1f} bp over {len(short_side)} hours")
if __name__ == "__main__":
try:
main()
except Exception as e: # a shape the example does not expect fails by name, not by traceback
fail(f"{type(e).__name__}: {e}")
import os
from datetime import datetime
from statistics import correlation, mean
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
API = "https://data.vike.io"
H = 3_600_000 # an hour in ms
def get(path, **params):
r = requests.get(API + path, params=params, headers={"X-API-Key": API_KEY}, timeout=120)
r.raise_for_status()
return r.json()
def ms(ts):
"""An ISO hour as epoch ms, the unit /ohlcv's `ts` is in."""
return int(datetime.fromisoformat(ts).timestamp() * 1000)
START, END = "2026-10-01T00:00:00Z", "2026-10-31T00:00:00Z" # 30 days, the window of the script above
COHORT = "3x rekt" # the cohort today's labels move to the other side most often (32% of hours, 2026-11-04)
c = get("/v1/hyperliquid/ohlcv", symbol="BTC", interval="1h", start=START, end="2026-10-31T02:00:00Z", limit=800)
ret = {int(b["ts"]): b["close"] / b["open"] - 1 for b in c["candles"]}
for basis in ("point_in_time", "current"): # each hour's own labels, then today's stamped on every hour
m = get("/v1/hl-cohorts/symbols/BTC/cohort-metrics", axis="pnl", labelBasis=basis, start=START, end=END, hours=800)
pairs = [(r["bias"], ret[ms(r["ts"]) + H]) for r in m["metrics"]
if r["cohort"] == COHORT and r["bias"] is not None and ms(r["ts"]) + H in ret]
up = [x for b, x in pairs if b > 0]
print(f"{basis:<13} lookahead={m['label_computed']['lookahead']!s:<5} {COHORT} net long {len(up)} of {len(pairs)} hours, "
f"corr(bias, next-hour return) {correlation(*zip(*pairs)):+.3f}")
Fade the losing cohort: mean reversion on extreme bias
Pull a coin's hourly realized-PnL matrix with labelBasis=point_in_time, take the 3x rekt rows' bias, and test whether its extremes are followed by a move the other way in /ohlcv.
- Uses
/symbols/{…}/cohort-metrics,/v1/hyperliquid/ohlcv- Verified
- live API, 2026-11-04: 4 calls, all 200
3x rekt bias was 2 standard deviations above its trailing week (34 events, 2026-08-14 to 11-03), BTC rose +11.1 bp over the next 6 hours on average and fading was right 11 of 34 times; after the most-short extremes (21) it rose +34.5 bp and fading was right 12 of 21. Every hour: +6.4 bp.The code
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# BTC's hourly pnl-cohort matrix over one week, labels as they were each hour
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/cohort-metrics?axis=pnl&labelBasis=point_in_time&start=2026-10-13T00:00:00Z&end=2026-10-19T23:59:59Z&hours=168",
headers={"X-API-Key": API_KEY},
)
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# BTC hourly candles over the same week plus six hours
r = requests.get(
"https://data.vike.io/v1/hyperliquid/ohlcv?symbol=BTC&interval=1h&start=2026-10-13T00:00:00Z&end=2026-10-20T06:00:00Z&limit=200",
headers={"X-API-Key": API_KEY},
)
import os
from datetime import datetime
from statistics import correlation, mean
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
API = "https://data.vike.io"
H = 3_600_000 # an hour in ms
def get(path, **params):
r = requests.get(API + path, params=params, headers={"X-API-Key": API_KEY}, timeout=120)
r.raise_for_status()
return r.json()
def ms(ts):
"""An ISO hour as epoch ms, the unit /ohlcv's `ts` is in."""
return int(datetime.fromisoformat(ts).timestamp() * 1000)
START, END = "2026-08-07T00:00:00Z", "2026-11-03T23:59:59Z" # 89 days; from 08-07 every pnl hour is labelled hourly
COHORT, WINDOW, Z, AHEAD = "3x rekt", 168, 2.0, 6 # extreme = 2 sd from its trailing week; the next 6 hours
# the cohort's bias every hour, each wallet labelled as it stood THAT hour, not today
m = get("/v1/hl-cohorts/symbols/BTC/cohort-metrics", axis="pnl", labelBasis="point_in_time",
start=START, end=END, hours=2200)
assert m["label_computed"]["lookahead"] is False
bias = {ms(r["ts"]): r["bias"] for r in m["metrics"] if r["cohort"] == COHORT and r["bias"] is not None}
# hourly closes, past the window's end for the last forward return (`end` is exclusive here)
c = get("/v1/hyperliquid/ohlcv", symbol="BTC", interval="1h", start=START, end="2026-11-05T00:00:00Z", limit=5000)
close = {int(b["ts"]): b["close"] for b in c["candles"]}
hours = sorted(bias)
every, events, skip_to = [], {"long": [], "short": []}, 0
for i, t in enumerate(hours[WINDOW:], WINDOW):
if t not in close or t + AHEAD * H not in close:
continue
ret = close[t + AHEAD * H] / close[t] - 1 # from the close of the bias hour
every.append(ret)
past = [bias[h] for h in hours[i - WINDOW:i]]
mu = mean(past)
sd = (sum((x - mu) ** 2 for x in past) / len(past)) ** 0.5
z = (bias[t] - mu) / sd if sd else 0
if t >= skip_to and abs(z) > Z: # one extreme is one event: skip the hours its forward return covers
events["long" if z > 0 else "short"].append(ret)
skip_to = t + AHEAD * H
print(f"{COHORT}, BTC, {len(every)} hours scored, labels point_in_time")
print(f" every hour: next {AHEAD}h {mean(every) * 1e4:+.1f} bp on average")
for side, xs in events.items():
if xs:
right = sum(1 for r in xs if (r < 0 if side == "long" else r > 0))
print(f" most {side} (z {'>' if side == 'long' else '<'} {'' if side == 'long' else '-'}{Z}): {len(xs)} events, "
f"next {AHEAD}h {mean(xs) * 1e4:+.1f} bp on average, fading it was right {right} of {len(xs)}")
Smart Money Index versus price: does it lead?
Pull a coin's hourly Smart Money Index from /indices for a pinned window (one page holds up to 5,000 hours) and its hourly candles from /ohlcv, then correlate SMI at hour t with the return over the next k hours.
- Uses
/symbols/{…}/indices,/v1/hyperliquid/ohlcv- Verified
- live API, 2026-11-04: 4 calls, all 200
The code
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# BTC's hourly Smart Money Index over 30 days, one page
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/indices?start=2026-10-05T00:00:00Z&end=2026-11-03T23:59:59Z",
headers={"X-API-Key": API_KEY},
)
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# BTC hourly candles over the same 30 days and one day past them; end is exclusive here
r = requests.get(
"https://data.vike.io/v1/hyperliquid/ohlcv?symbol=BTC&interval=1h&start=2026-10-05T00:00:00Z&end=2026-11-05T00:00:00Z&limit=800",
headers={"X-API-Key": API_KEY},
)
import os
from datetime import datetime
from statistics import correlation, mean
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
API = "https://data.vike.io"
H = 3_600_000 # an hour in ms
def get(path, **params):
r = requests.get(API + path, params=params, headers={"X-API-Key": API_KEY}, timeout=120)
r.raise_for_status()
return r.json()
def ms(ts):
"""An ISO hour as epoch ms, the unit /ohlcv's `ts` is in."""
return int(datetime.fromisoformat(ts).timestamp() * 1000)
START, END = "2026-08-07T00:00:00Z", "2026-11-03T23:59:59Z" # 89 days; from 08-07 every pnl hour is labelled hourly
s = get("/v1/hl-cohorts/symbols/BTC/indices", start=START, end=END)
assert not s["has_more"] # 2,136 hours: one page (up to 5,000 rows)
smi = {ms(r["ts"]): r["smi"] for r in s["series"] if r["smi"] is not None}
c = get("/v1/hyperliquid/ohlcv", symbol="BTC", interval="1h", start=START, end="2026-11-05T00:00:00Z", limit=5000)
close = {int(b["ts"]): b["close"] for b in c["candles"]}
print(f"BTC Smart Money Index, {len(smi)} hours, {START[:10]} to {END[:10]}")
for k in (1, 4, 12, 24):
pairs = [(v, close[t + k * H] / close[t] - 1) for t, v in smi.items() if t in close and t + k * H in close]
print(f" corr(smi at t, return t -> t+{k}h) {correlation(*zip(*pairs)):+.3f} over {len(pairs)} hours")
# the fifth of hours with the highest SMI against the fifth with the lowest, next 24 hours
pairs = sorted((v, close[t + 24 * H] / close[t] - 1) for t, v in smi.items() if t in close and t + 24 * H in close)
n = len(pairs) // 5
print(f" next 24h after the top fifth of SMI {mean(r for _, r in pairs[-n:]) * 1e4:+.0f} bp, "
f"after the bottom fifth {mean(r for _, r in pairs[:n]) * 1e4:+.0f} bp")
Are Whale-vs-Shrimp extremes a contrarian signal?
Take the hourly wsi series from /indices, z-score it against its own trailing month, and check whether the 24-hour return from /ohlcv after an extreme goes against the side the index leaned.
- Uses
/symbols/{…}/indices,/v1/hyperliquid/ohlcv- Verified
- live API, 2026-11-04: 5 calls, all 200
The code
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# ETH's hourly indices over four hours of 2026-10-20
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/ETH/indices?start=2026-10-20T00:00:00Z&end=2026-10-20T03:00:00Z",
headers={"X-API-Key": API_KEY},
)
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# ETH's hourly indices from 2026-08-07 to 2026-11-03: 2,136 hours, one page
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/ETH/indices?start=2026-08-07T00:00:00Z&end=2026-11-03T23:59:59Z",
headers={"X-API-Key": API_KEY},
)
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# ETH hourly candles over the same window and one day past it
r = requests.get(
"https://data.vike.io/v1/hyperliquid/ohlcv?symbol=ETH&interval=1h&start=2026-08-07T00:00:00Z&end=2026-11-05T00:00:00Z&limit=5000",
headers={"X-API-Key": API_KEY},
)
import os
from datetime import datetime
from statistics import correlation, mean
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
API = "https://data.vike.io"
H = 3_600_000 # an hour in ms
def get(path, **params):
r = requests.get(API + path, params=params, headers={"X-API-Key": API_KEY}, timeout=120)
r.raise_for_status()
return r.json()
def ms(ts):
"""An ISO hour as epoch ms, the unit /ohlcv's `ts` is in."""
return int(datetime.fromisoformat(ts).timestamp() * 1000)
START, END = "2026-08-07T00:00:00Z", "2026-11-03T23:59:59Z" # 89 days; from 08-07 every pnl hour is labelled hourly
WINDOW, Z, AHEAD = 720, 2.0, 24 # z against the trailing 30 days; the next 24 hours
s = get("/v1/hl-cohorts/symbols/ETH/indices", start=START, end=END)
assert not s["has_more"]
wsi = {ms(r["ts"]): r["wsi"] for r in s["series"] if r["wsi"] is not None} # null when a side holds nothing
c = get("/v1/hyperliquid/ohlcv", symbol="ETH", interval="1h", start=START, end="2026-11-05T00:00:00Z", limit=5000)
close = {int(b["ts"]): b["close"] for b in c["candles"]}
hours = sorted(wsi)
events, skip_to, scored = [], 0, 0
for i, t in enumerate(hours[WINDOW:], WINDOW):
if t not in close or t + AHEAD * H not in close:
continue
scored += 1
past = [wsi[h] for h in hours[i - WINDOW:i]]
mu = mean(past)
sd = (sum((x - mu) ** 2 for x in past) / len(past)) ** 0.5
z = (wsi[t] - mu) / sd if sd else 0
if t >= skip_to and abs(z) > Z: # count events, not hours: forward returns of neighbouring hours overlap
events.append((z, close[t + AHEAD * H] / close[t] - 1))
skip_to = t + AHEAD * H
print(f"ETH Whale-vs-Shrimp Index, {scored} hours scored, {len(events)} extremes (|z| > {Z})")
for name, xs in (("whales most long", [r for z, r in events if z > 0]), ("whales most short", [r for z, r in events if z < 0])):
if xs:
against = sum(1 for r in xs if (r < 0 if name.endswith("long") else r > 0))
print(f" {name}: {len(xs)} events, next {AHEAD}h {mean(xs) * 1e4:+.0f} bp on average, "
f"price went against the whales {against} of {len(xs)}")
Hyperliquid net flow by cohort, hour by hour
Difference each cohort's long and short size from one cohort-metrics call; an hour's flow is the change in long minus the change in short, in tokens of the coin, positive when the cohort is buying.
- Uses
/symbols/{…}/cohort-metrics,/v1/hyperliquid/ohlcv- Verified
- live API, 2026-11-04: 5 calls, all 200
The code
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# BTC open position size by account-value cohort, 25 hourly buckets
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/cohort-metrics?axis=size&hours=25",
headers={"X-API-Key": API_KEY},
)
"""Net buying or selling of one coin by cohort, hour by hour: the change in long size minus the change in short size.
Run: VIKE_API_KEY=... python hl-cohort-net-flow.py (--dry-run lists the requests and makes none)
"""
import os
import sys
from datetime import datetime, timezone
from urllib.parse import urlencode
NAME = "hl-cohort-net-flow"
BASE = "https://data.vike.io"
DRY = "--dry-run" in sys.argv
COIN = "BTC"
def fail(why):
sys.exit(f"{NAME}: {why}")
def call(path, method="GET", **params):
url = BASE + path + ("?" + urlencode(params) if params else "")
if DRY:
print(method, url)
return None
key = os.environ.get("VIKE_API_KEY") or fail("set VIKE_API_KEY")
import requests
r = requests.request(method, url, headers={"X-API-Key": key}, timeout=60)
if r.status_code != 200:
fail(f"{method} {path} answered HTTP {r.status_code}: {r.text[:200]}")
return r.json()
def net_flow(metrics: list[dict]) -> dict[str, list[tuple[int, float]]]:
"""cohort -> [(epoch seconds of the hour, flow)], flow in tokens of the coin, + = buying.
flow(t) = (long(t) - long(prev)) - (short(t) - short(prev)). The grid is the hours present in `metrics`;
a cohort with no row in one of them held nothing there (0, not a hole); an hour missing for the whole
coin is a gap the next hour's flow spans; the first hour is a baseline and gets no point.
"""
sizes = {} # cohort -> {hour: (long, short)}
for m in metrics:
if m["cohort"] in ("unknown", ""): # `unknown` (pnl: under 10 closed trades; size: on some hours, cause not established here) and '': dropped on the site too
continue
for field in ("total_position_size", "total_position_size_long"):
if m[field] is None:
fail(f"{field} is null for {m['cohort']} at {m['ts']} (a point-in-time pnl matrix has none before 2026-08-07)")
total, long_ = m["total_position_size"], m["total_position_size_long"]
hour = int(datetime.fromisoformat(m["ts"]).timestamp())
sizes.setdefault(m["cohort"], {})[hour] = (long_, total - long_)
hours = sorted({h for by_hour in sizes.values() for h in by_hour})
flows = {}
for cohort, by_hour in sizes.items():
points = []
for prev, t in zip(hours, hours[1:]):
l0, s0 = by_hour.get(prev, (0.0, 0.0))
l1, s1 = by_hour.get(t, (0.0, 0.0))
points.append((t, round((l1 - l0) - (s1 - s0), 6))) # rounded to 6 places, as the website does
flows[cohort] = points
return flows
def main():
# 25 hourly buckets give 24 changes: the oldest hour is only the baseline of the first one.
matrix = call(f"/v1/hl-cohorts/symbols/{COIN}/cohort-metrics", axis="size", hours=25)
if DRY:
return
flows = net_flow(matrix["metrics"])
if not any(flows.values()):
fail(f"fewer than two hourly buckets of {COIN} came back")
stamp = lambda t: datetime.fromtimestamp(t, timezone.utc).strftime("%Y-%m-%d %H:%M")
some = next(iter(flows.values())) # every cohort has a point at every hour of the grid
print(f"{COIN} net flow by cohort, {len(some)} hourly changes, {stamp(some[0][0])} to {stamp(some[-1][0])} UTC")
print(f"tokens of {COIN}; positive = buying (longs up or shorts down)")
print(f" {'cohort':<11}{'last hour':>12}{'window':>12}")
for cohort, pts in flows.items():
print(f" {cohort:<11}{pts[-1][1]:>+12,.2f}{sum(f for _, f in pts):>+12,.2f}")
if __name__ == "__main__":
try:
main()
except Exception as e: # a shape the example does not expect fails by name, not by traceback
fail(f"{type(e).__name__}: {e}")
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# the same matrix on the realized-PnL ladder, each hour labelled as it was that hour
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/cohort-metrics?axis=pnl&labelBasis=point_in_time&hours=25",
headers={"X-API-Key": API_KEY},
)
import os
from datetime import datetime
from statistics import correlation, mean
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
API = "https://data.vike.io"
H = 3_600_000 # an hour in ms
def get(path, **params):
r = requests.get(API + path, params=params, headers={"X-API-Key": API_KEY}, timeout=120)
r.raise_for_status()
return r.json()
def ms(ts):
"""An ISO hour as epoch ms, the unit /ohlcv's `ts` is in."""
return int(datetime.fromisoformat(ts).timestamp() * 1000)
START, END = "2026-10-04T00:00:00Z", "2026-11-03T23:59:59Z" # 30 days
WHALES = {"4x whale", "3x whale", "2x whale", "whale"}
SHRIMP = {"2x shrimp", "shrimp"}
m = get("/v1/hl-cohorts/symbols/BTC/cohort-metrics", axis="size", start=START, end=END, hours=800)
c = get("/v1/hyperliquid/ohlcv", symbol="BTC", interval="1h", start=START, end="2026-11-05T00:00:00Z", limit=5000)
close = {int(b["ts"]): b["close"] for b in c["candles"]}
# each group's long and short size in BTC per hour; a cohort missing from an hour held nothing (0)
size = {"whales": {}, "shrimp": {}}
for r in m["metrics"]:
g = "whales" if r["cohort"] in WHALES else "shrimp" if r["cohort"] in SHRIMP else None
if g:
long_, total = r["total_position_size_long"], r["total_position_size"]
acc = size[g].setdefault(ms(r["ts"]), [0.0, 0.0])
acc[0] += long_
acc[1] += total - long_
hours = sorted({ms(r["ts"]) for r in m["metrics"]})
print(f"BTC, {len(hours) - 1} hourly changes, {START[:10]} to {END[:10]}; flow in BTC, + = buying")
for g, by in size.items():
flow = {}
for prev, t in zip(hours, hours[1:]):
(l0, s0), (l1, s1) = by.get(prev, (0.0, 0.0)), by.get(t, (0.0, 0.0))
flow[t] = (l1 - l0) - (s1 - s0)
same = [(f, close[t] / close[t - H] - 1) for t, f in flow.items() if t in close and t - H in close]
nxt = [(f, close[t + H] / close[t] - 1) for t, f in flow.items() if t in close and t + H in close]
print(f" {g:<7} corr(flow, the same hour's return) {correlation(*zip(*same)):+.3f}, "
f"corr(flow, the next hour's return) {correlation(*zip(*nxt)):+.3f}")
Backtest following smart money with point-in-time cohort labels from the positions archive
Download a day of the positions archive; every 5-minute position row carries pnl_cohort, size_cohort and account_value as they stood at the start of its hour, so a backtest never reads a label from the future.
- Uses
/v1/hyperliquid/archive/datasets,/v1/hyperliquid/archive/url,/v1/hyperliquid/ohlcv,/v1/hyperliquid/positions- Verified
- live API, 2026-11-04: 11 calls, all 200
The code
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# a 7-day link to every 5-minute position snapshot of 2026-10-23
r = requests.get(
"https://data.vike.io/v1/hyperliquid/archive/url?dataset=positions&date=2026-10-23",
headers={"X-API-Key": API_KEY},
)
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# the newest day that has a positions file
r = requests.get(
"https://data.vike.io/v1/hyperliquid/archive/datasets",
headers={"X-API-Key": API_KEY},
)
# then: jq '[.dates[] | select(.streams.positions)][0]'
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# BTC positions held by 3x smart wallets in the 12:00 hour of 2026-10-22, one row per wallet and hour
r = requests.get(
"https://data.vike.io/v1/hyperliquid/positions?symbol=BTC&axis=pnl&cohort=3x_smart&start=2026-10-22T12:00:00Z&end=2026-10-22T12:59:59Z&resolution=1h&limit=3",
headers={"X-API-Key": API_KEY},
)
import os
from statistics import correlation
import polars as pl
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
API = "https://data.vike.io"
DAYS = ("2026-10-28", "2026-10-29", "2026-10-30", "2026-10-31", "2026-11-01", "2026-11-02", "2026-11-03")
SMART = ["3x smart", "2x smart", "smart"]
H = 3_600_000
def get(path, **params):
r = requests.get(API + path, params=params, headers={"X-API-Key": API_KEY}, timeout=120)
r.raise_for_status()
return r.json()
# the smart cohorts' net BTC position at every 5-minute snapshot, each row labelled as of the start of its hour
book = []
for day in DAYS:
url = get("/v1/hyperliquid/archive/url", dataset="positions", date=day)["url"]
book.append(pl.scan_parquet(url) # polars reads only BTC's byte ranges off the link
.filter((pl.col("asset") == "BTC") & pl.col("pnl_cohort").is_in(SMART))
.group_by("snapshot_ts").agg((pl.col("direction") * pl.col("size_token")).sum().alias("net_btc"))
.collect())
book = pl.concat(book).sort("snapshot_ts")
# the book at the hour's last snapshot; its change over the hour is the signal
hourly = (book.with_columns(pl.col("snapshot_ts").dt.truncate("1h").dt.epoch("ms").alias("t"))
.group_by("t", maintain_order=True).agg(pl.col("net_btc").last()))
net = dict(zip(hourly["t"].to_list(), hourly["net_btc"].to_list()))
c = get("/v1/hyperliquid/ohlcv", symbol="BTC", interval="1h", start=f"{DAYS[0]}T00:00:00Z",
end="2026-11-04T02:00:00Z", limit=500)
close = {int(b["ts"]): b["close"] for b in c["candles"]}
pairs = [(net[t] - net[t - H], close[t + H] / close[t] - 1) for t in net
if t - H in net and t in close and t + H in close]
print(f"smart cohorts on BTC, {book.height:,} snapshots over {len(DAYS)} days: net {book['net_btc'][0]:+,.0f} BTC "
f"at the first, {book['net_btc'][-1]:+,.0f} BTC at the last")
print(f" corr(the hour's change in their net BTC, the next hour's return) {correlation(*zip(*pairs)):+.3f} "
f"over {len(pairs)} hours")
Do smart-money wallets' large fills precede price moves?
/large-fills holds a coin's newest 200 fills over $50,000, so live you log it on a timer; for past days, label the fills archive with the positions archive's point-in-time pnl_cohort and measure the price 10 minutes after each smart-money taker fill with 1-minute /ohlcv.
- Uses
/symbols/{…}/large-fills,/v1/hyperliquid/archive/url,/v1/hyperliquid/ohlcv- Verified
- live API, 2026-11-04: 13 calls, all 200
The code
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# every BTC fill over $50,000 the feed holds; run it every few minutes and append the new rows to your log
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/large-fills",
headers={"X-API-Key": API_KEY},
)
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# ten minutes of BTC 1-minute candles around the captured fills; end is exclusive
r = requests.get(
"https://data.vike.io/v1/hyperliquid/ohlcv?symbol=BTC&interval=1m&start=2026-10-24T17:10:00Z&end=2026-10-24T17:20:00Z",
headers={"X-API-Key": API_KEY},
)
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# BTC fills of $250,000 or more still in the feed
r = requests.get(
"https://data.vike.io/v1/hl-cohorts/symbols/BTC/large-fills?min_usd=250000",
headers={"X-API-Key": API_KEY},
)
import os
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
# a temporary link to every fill of 2026-10-23
r = requests.get(
"https://data.vike.io/v1/hyperliquid/archive/url?dataset=fills&date=2026-10-23",
headers={"X-API-Key": API_KEY},
)
import os
from datetime import datetime, timedelta
from statistics import mean
import polars as pl
import requests
API_KEY = os.environ["VIKE_API_KEY"] # your key, read from the environment
API = "https://data.vike.io"
DAYS = ("2026-11-01", "2026-11-02", "2026-11-03")
SMART = ("3x smart", "2x smart", "smart")
MIN_USD, AHEAD = 50_000, 10 # the large-fills floor; the price 10 minutes later
def get(path, **params):
r = requests.get(API + path, params=params, headers={"X-API-Key": API_KEY}, timeout=120)
r.raise_for_status()
return r.json()
def btc(dataset, day, cols):
"""Only BTC's part of one archive file: polars reads the needed byte ranges straight off the link."""
url = get("/v1/hyperliquid/archive/url", dataset=dataset, date=day)["url"]
return pl.scan_parquet(url).filter(pl.col("asset") == "BTC").select(cols).collect()
def hour(col):
return pl.col(col).dt.replace_time_zone(None).dt.cast_time_unit("ms").dt.truncate("1h").alias("hour")
moves = {"smart wallets": [], "every wallet": []}
labelled = total = 0
for day in DAYS:
# the taker side of every BTC trade of $50k or more (the fills file carries no cohort)
fills = (btc("fills", day, ["fill_ts", "wallet", "side", "size_usd", "price", "crossed"])
.filter(pl.col("crossed") & (pl.col("size_usd") >= MIN_USD))
.with_columns(hour("fill_ts"), (pl.col("fill_ts").dt.epoch("ms") // 60_000 * 60_000).alias("minute")))
# the label: the positions file's pnl_cohort, graded on fills strictly BEFORE the row's hour, so it cannot see the fill
labels = (btc("positions", day, ["snapshot_ts", "wallet", "pnl_cohort"])
.with_columns(hour("snapshot_ts")).group_by("wallet", "hour").agg(pl.col("pnl_cohort").first()))
f = fills.join(labels, on=["wallet", "hour"], how="left")
total += f.height
labelled += f.filter(pl.col("pnl_cohort").is_not_null()).height
end = (datetime.fromisoformat(day) + timedelta(days=1, minutes=AHEAD + 1)).strftime("%Y-%m-%dT%H:%M:%SZ")
bars = get("/v1/hyperliquid/ohlcv", symbol="BTC", interval="1m", start=f"{day}T00:00:00Z", end=end, limit=5000)
close = {int(b["ts"]): b["close"] for b in bars["candles"]}
for row in f.iter_rows(named=True):
later = close.get(row["minute"] + AHEAD * 60_000)
if later is None:
continue
move = (1 if row["side"] == "buy" else -1) * (later / row["price"] - 1) # + = price went the fill's way
moves["every wallet"].append(move)
if row["pnl_cohort"] in SMART:
moves["smart wallets"].append(move)
print(f"BTC taker fills of ${MIN_USD:,}+ on {DAYS[0]} to {DAYS[-1]}: {total:,}, {labelled / total:.0%} carry a label")
for who, xs in moves.items():
print(f" {who:<13} {len(xs):>6,} fills: {AHEAD} min later the price had moved their way "
f"{mean(xs) * 1e4:+.2f} bp on average, {sum(x > 0 for x in xs) / len(xs):.0%} of the time")
Download every position with its labels
One file per UTC day from 2026-05-08: every open position at every 5-minute snapshot, three point-in-time columns on every row.
| Column | What it holds |
|---|---|
size_cohort | Account-value size tier of the wallet WHEN THE ROW WAS WRITTEN (shrimp, 2x shrimp, fish, ... up to 4x whale: 12 absolute-dollar thresholds assigned at ingestion), as stored: a point-in-time label, not today's grading. The same field and vocabulary as size_cohort in /v1/hyperliquid/positions. |
pnl_cohort | Realized-PnL percentile cohort of the wallet AS OF THE START OF THE ROW'S CLOCK HOUR: graded on fills strictly before the row's hour (the hourly label of the previous hour). This is ONE HOUR EARLIER than the label /v1/hyperliquid/positions returns for the same row in historical mode (start given), where a row graded from the hourly label table carries pnl_cohort_basis = point_in_time: that label is the row's own hour and so counts the fills of up to 59 minutes after the row. In latest, as of the start of the hour of the file's newest snapshot (an older hour while the label rollup lags), so on the rows of positions that closed in the last 15 minutes it can count up to 15 minutes of fills after the row. unknown for a wallet with fewer than 10 closed trades then, or with no realized record that hour; never empty. |
account_value | Native Hyperliquid perp account value of the wallet, USD (the quantity /v1/hyperliquid/wallets/{address} serves as equity.account_value), AS OF THE START OF THE ROW'S CLOCK HOUR: the wallet's last snapshot of the previous hour (a row at 10:37 carries the 09:5x value). Builder-DEX account equity is NOT included, so a wallet trading only dex:NAME assets usually reads 0. In latest, as of the start of the hour of the file's newest snapshot (an older hour while the hourly rollup lags), so on the rows of positions that closed in the last 15 minutes it can be up to 15 minutes later than the row. NULL when unknown: the wallet had no snapshot in that previous hour (e.g. its first position opened in the row's hour); the stored value is a reconstruction, not a measurement (hours rebuilt from fills, and the values the 2026-08-10 repair rewrote); or the row is a builder-DEX position (dex_name other than HL) whose value comes from an hour up to 2026-08-10 00:48 UTC (the repair seam): until then a builder-DEX-only wallet's empty native account was stored as a non-equity figure the repair did not catch every time, and a genuine balance cannot be told apart from a fabricated one, so genuine values are withheld on those rows too. The same wallet's HL rows in the same snapshot keep the value; only its builder-DEX rows read NULL. |
import os, requests
import polars as pl
link = requests.get(
"https://data.vike.io/v1/hyperliquid/archive/url",
params={"dataset": "positions", "date": "2026-11-03"},
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
timeout=30,
).json()["url"] # needs no key, works 7 days
# polars reads only the byte ranges it needs: BTC's rows of a 1.6-1.9 GB file
book = (pl.scan_parquet(link)
.filter(pl.col("asset") == "BTC")
.group_by("snapshot_ts", "pnl_cohort")
.agg((pl.col("direction") * pl.col("size_usd")).sum().alias("net_usd"))
.sort("snapshot_ts")
.collect())
print(book.filter(pl.col("pnl_cohort").is_in(["3x smart", "2x smart", "smart"])).tail(6))
import os, requests
import polars as pl
def link(dataset):
return requests.get(
"https://data.vike.io/v1/hyperliquid/archive/url",
params={"dataset": dataset, "date": "2026-11-03"},
headers={"X-API-KEY": os.environ["VIKE_API_KEY"]},
timeout=30,
).json()["url"]
hour = lambda c: pl.col(c).dt.replace_time_zone(None).dt.cast_time_unit("ms").dt.truncate("1h").alias("hour")
fills = (pl.scan_parquet(link("fills")).filter(pl.col("asset") == "BTC")
.select("fill_ts", "wallet", "side", "size_usd", "crossed").with_columns(hour("fill_ts")).collect())
# a wallet's label as of the START of the hour: graded on fills strictly before it, so it cannot see the fill
labels = (pl.scan_parquet(link("positions")).filter(pl.col("asset") == "BTC")
.select("snapshot_ts", "wallet", "pnl_cohort").with_columns(hour("snapshot_ts"))
.group_by("wallet", "hour").agg(pl.col("pnl_cohort").first()).collect())
labelled = fills.join(labels, on=["wallet", "hour"], how="left")
print(labelled.group_by("pnl_cohort").agg(pl.len(), pl.col("size_usd").sum()).sort("size_usd", descending=True))
# a link to one day (the API answers 302 to it with redirect=1); -L follows it
curl -L -o positions-2026-11-03.parquet -H "X-API-KEY: $VIKE_API_KEY" \
"https://data.vike.io/v1/hyperliquid/archive/url?dataset=positions&date=2026-11-03&redirect=1"
-- positions-2026-11-03.parquet downloaded first (see the cURL tab), then read locally.
-- The smart cohorts' net BTC book at every snapshot of the day (direction: 1 long, -1 short).
SELECT snapshot_ts,
sum(direction * size_token) AS net_btc
FROM 'positions-2026-11-03.parquet'
WHERE asset = 'BTC' AND pnl_cohort IN ('3x smart', '2x smart', 'smart')
GROUP BY snapshot_ts
ORDER BY snapshot_ts;
vike-data hl archive --view url --dataset positions --date 2026-11-03
- The link needs no key and works 7 days; asking for it is one request, the download itself spends none.
- Snapshots are not evenly spaced, and a source outage that could not be rebuilt leaves a gap that is published as it is:
snapshotson the day's entry counts them. A position held across snapshots appears in each.account_valueandpnl_cohortare as of the START of the row's clock hour (equity and fills strictly before it;pnl_cohortis one hour earlier than the API's point-in-time label; meanings underschema.positions). Hours the source rebuilt by replaying fills instead of reading a node state file are listed inreconstructed_hourson the day's entry (reconstructed, not measured:margin_usedandliquidation_priceare 0,state_tsis 1970-01-01). Sizes matched the next real snapshot at 97 % or better, except the hours also listed inreconstructed_lower_match_hours, which matched at 83-97 %. - The fills files (from 2026-05-01) carry no cohort: the “Label the fills” tab joins them to the positions file's labels on wallet and hour.
- Free samples, no key: fills-sample.parquet · positions-sample.parquet · manifest.json.
- Every column of both files, the fills download and the candles: Hyperliquid historical data.
Hyperliquid backtest FAQ
What goes wrong if a backtest uses today's labels?
3x rekt sat on the other side of the market in 32.2% of hours under today's labels. Pass labelBasis=point_in_time, and check label_computed.lookahead: false in the answer.How far back does Hyperliquid cohort data go?
Why do the labels start on 7 April 2026?
What is the difference between realized and unrealized grading?
grading=realized ranks wallets by cumulative closed PnL (at least 10 closed trades, else unknown): who has been good. grading=unrealized ranks them by the unrealized PnL of their open positions as a share of account value: who is onside now. It turns over about 44% of wallets a day against about 2.4% for the realized ladder, and part of it restates the move that just happened, so read it as positioning, not skill.Are Hyperliquid fills labelled with a cohort?
Can I run these backtests from Claude or ChatGPT?
hl_cohort_metrics with label_basis=point_in_time answers the cohort questions, hl_market view=indices the indices, hl_ohlcv the candles and hl_archive the download links, on one free key.