vike

Example 28 of 34

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.

GET /v1/hl-cohorts/symbols/{symbol}/cohort-metrics answers it in one call: every hourly row carries a cohort's total_position_size and the long part of it, total_position_size_long, both in tokens of the coin. The short part is the difference. For each cohort, an hour's flow is the change in long size since the hour before minus the change in short size: positive when the cohort bought (longs up or shorts down), negative when it sold. The full parameter list is on Coin cohort metrics.

Quick answer

Ask for 25 hourly buckets: the oldest is only the baseline, so they make 24 hourly changes.

cURL
# BTC open position size by account-value cohort, 25 hourly buckets
curl -H "X-API-Key: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/symbols/BTC/cohort-metrics?axis=size&hours=25"

Example response

Captured from the live API on 2026-10-27 at 10:56 UTC, trimmed to the 4x whale and 3x whale cohorts of the newest 3 hours (6 rows of 300). Between 08:00 and 09:00 the 4x whale long size fell from 3,749.37 to 3,721.18 BTC (-28.1907) and its short size, total_position_size minus total_position_size_long, rose from 10,650.95 to 10,675.60 BTC (+24.6548), so its flow was -28.1907 - 24.6548 = -52.8455 BTC: selling. nextCursor is set because the page holds as many hours as were asked for; the recipe does not page. The newest bucket is 09:00: the hour in progress is never returned.

Example response
{
  "axis": "size",
  "label_basis": "point_in_time",
  "metrics": [
    {
      "ts": "2026-10-27T09:00:00+00:00",
      "cohort": "4x whale",
      "position_count": 23,
      "position_count_long": 7,
      "total_position_value": 1147230552.0,
      "total_position_value_long": 296528067.4,
      "total_unrealized_pnl": -9946951.504,
      "total_position_size": 14396.78129,
      "total_position_size_long": 3721.17856,
      "bias": -0.4830540959,
      "label_basis": "point_in_time",
      "grading": null
    },
    {
      "ts": "2026-10-27T09:00:00+00:00",
      "cohort": "3x whale",
      "position_count": 25,
      "position_count_long": 11,
      "total_position_value": 222770980.8,
      "total_position_value_long": 144035872.3,
      "total_unrealized_pnl": 14168276.86,
      "total_position_size": 2795.58898,
      "total_position_size_long": 1807.5294,
      "bias": 0.2931295787,
      "label_basis": "point_in_time",
      "grading": null
    },
    {
      "ts": "2026-10-27T08:00:00+00:00",
      "cohort": "4x whale",
      "position_count": 23,
      "position_count_long": 7,
      "total_position_value": 1148971066.0,
      "total_position_value_long": 299154299.6,
      "total_unrealized_pnl": -10655654.18,
      "total_position_size": 14400.31717,
      "total_position_size_long": 3749.36926,
      "bias": -0.479265739,
      "label_basis": "point_in_time",
      "grading": null
    },
    {
      "ts": "2026-10-27T08:00:00+00:00",
      "cohort": "3x whale",
      "position_count": 25,
      "position_count_long": 11,
      "total_position_value": 219688224.9,
      "total_position_value_long": 144653788.7,
      "total_unrealized_pnl": 14249543.26,
      "total_position_size": 2753.40277,
      "total_position_size_long": 1812.97902,
      "bias": 0.3169007017,
      "label_basis": "point_in_time",
      "grading": null
    },
    {
      "ts": "2026-10-27T07:00:00+00:00",
      "cohort": "4x whale",
      "position_count": 23,
      "position_count_long": 7,
      "total_position_value": 1151367647.0,
      "total_position_value_long": 302788216.6,
      "total_unrealized_pnl": -11373018.52,
      "total_position_size": 14411.56922,
      "total_position_size_long": 3789.97391,
      "bias": -0.4740373026,
      "label_basis": "point_in_time",
      "grading": null
    },
    {
      "ts": "2026-10-27T07:00:00+00:00",
      "cohort": "3x whale",
      "position_count": 25,
      "position_count_long": 12,
      "total_position_value": 214136216.9,
      "total_position_value_long": 145889764.5,
      "total_unrealized_pnl": 14361118.53,
      "total_position_size": 2680.3245,
      "total_position_size_long": 1826.08956,
      "bias": 0.3625884179,
      "label_basis": "point_in_time",
      "grading": null
    }
  ],
  "hours": 25,
  "nextCursor": "eyJtIjoiY29ob3J0X21ldHJpY3MiLCJ0IjoiMjAyNi0xMC0yNiAwOTowMDowMCJ9",
  "grading": null,
  "freshness": {
    "newest_ts": "2026-10-27T09:00:00+00:00",
    "behind_hours": 1,
    "settled_lag_ceiling_hours": 3,
    "stale": false
  }
}

The script makes this one call, differences every cohort hour over hour and prints each cohort's newest flow and its sum over the window. That sum is the net change of the whole window: the long size at the end minus the long size at the start, minus the same for the short side. The math is the one behind the Net Flow charts on the vike.io asset pages.

Run
export VIKE_API_KEY=...
python hl-cohort-net-flow.py             # real run: needs the key
python hl-cohort-net-flow.py --dry-run   # prints the request it would make, calls nothing
hl-cohort-net-flow.py
"""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}")

A run at 11:04 UTC on 2026-10-27 printed one line per size cohort. Over the 24 hours 3x whale sold 1,184.84 BTC net while 4x whale bought 649.30; in the newest hour both sold about 53 BTC.

Output
BTC net flow by cohort, 24 hourly changes, 2026-10-26 10:00 to 2026-10-27 09:00 UTC
tokens of BTC; positive = buying (longs up or shorts down)
  cohort        last hour      window
  4x whale         -52.85     +649.30
  3x whale         -53.09   -1,184.84
  2x whale        -172.16       -7.02
  whale           +382.22     +408.73
  2x shark        -131.12     -372.21
  shark           -149.40      -33.27
  2x dolphin      +210.73     +197.27
  dolphin          -44.77      +54.95
  2x fish          -18.49      -97.69
  fish              +9.54     +122.25
  2x shrimp        +30.67     +212.88
  shrimp           +70.87      +67.20

Variations

The PnL ladder. The same recipe runs on the realized-PnL cohorts: change the call in the script to axis="pnl", labelBasis="point_in_time". labelBasis=point_in_time labels every hour as it was that hour, so the cohort a wallet is counted in does not change with today's grading. The pnl response also carries an unknown cohort (wallets with under 10 closed trades, which have no PnL grade), and a size response can carry one too, on some hours (see Coin cohort metrics); the script drops both.

cURL — pnl ladder
# the same matrix on the realized-PnL ladder, each hour labelled as it was that hour
curl -H "X-API-Key: $VIKE_API_KEY" \
  "https://data.vike.io/v1/hl-cohorts/symbols/BTC/cohort-metrics?axis=pnl&labelBasis=point_in_time&hours=25"

Before you rely on it

Rebuilt hours show up as a run of reversing flows

The Net Flow charts on the vike.io asset pages leave out the hours that were rebuilt from fills, carried forward or only partly measured, and cohort-metrics returns those hours with nothing to mark them. A cohort that loses size in one hour and gets it back a few hours later while the coin's total size hardly moves is that artifact, not trading. On 2026-10-09 the hours from 17:00 to 22:00 UTC were rebuilt. Read on 2026-10-27, the recipe's flow for BTC's 3x whale cohort is -1,769.01 BTC at 17:00, -377.76 BTC at 18:00 and +2,863.03 BTC at 19:00, while the total size of all the named cohorts together moved by at most 229 BTC from one hour to the next between 16:00 and 19:00.

Do not delete those points after differencing: that breaks the identity between the window sum and the window's net change. Take the rebuilt hours out first and difference across them, so the next measured hour's flow spans the gap; the charts do this for every hour recorded as rebuilt, carried forward or partly measured. A day's reconstructed_hours in /archive/datasets lists only the hours rebuilt from fills; carried-forward and partly measured hours are not in that list (Freshness). The script does not do this: it differences every hour it is given.

A cohort missing from an hour held nothing in it

A cohort is returned in an hour only if it holds a position in the coin: there is no row of zeros. On 2026-10-27 BTC had all twelve size cohorts in every one of its 25 hours, but GAS had no dolphin row in 6 of them and no 4x whale row in any. The script reads a missing cohort as long 0 and short 0, so a cohort that closes everything is selling and one that returns is buying. Differencing only the rows a cohort has would hide both its exit and its return.

The hour in progress is never returned, and closes arrive late

An hour that has just ended appears once it has been written: at 10:56 UTC the newest bucket was 09:00 and freshness.behind_hours was 1. A row is a wallet's last sighting inside its hour, so a position closed at 10:40 still counts in the 10:00 row and is gone from the 11:00 one: closes reach the flow up to an hour after opens do.

Flow is the change in a cohort's open size, not a count of trades

The unit is the coin, so price does not move it. A wallet that grows from whale into 2x whale between two hours leaves one cohort and joins the other, and shows as selling in the first and buying in the second; across all the cohorts together those moves cancel.

A pnl matrix has no size fields before 2026-08-07

With axis=pnl and labelBasis=point_in_time, a window that reaches before 2026-08-07 comes from an aggregate where total_position_size and total_position_size_long are null, listed in unavailable_fields. The script stops on the first null, with a message that names the example, rather than reading it as 0. axis=size is built from the position rows themselves and carries both fields.

Ask your agent

Connected to the Vike MCP server (https://data.vike.io/mcp) in Claude, ChatGPT or Cursor, ask it in plain words. The hl_cohort_metrics tool answers, at 1 credit a call.

Ask your agent
For BTC, take each size cohort's long and short position size for the last 25 hourly buckets and compute its 24 hourly net flows in BTC: the change in long minus the change in short
Example response
{  "axis": "size",  "label_basis": "point_in_time",  "metrics": [    {      "ts": "2026-10-27T09:00:00+00:00",      "cohort": "4x whale",      "position_count": 23,      "position_count_long": 7,      "total_position_value": 1147230552.0,      "total_position_value_long": 296528067.4,      "total_unrealized_pnl": -9946951.504,      "total_position_size": 14396.78129,      "total_position_size_long": 3721.17856,      "bias": -0.4830540959,      "label_basis": "point_in_time",      "grading": null    },    {      "ts": "2026-10-27T09:00:00+00:00",      "cohort": "3x whale",      "position_count": 25,      "position_count_long": 11,      "total_position_value": 222770980.8,      "total_position_value_long": 144035872.3,      "total_unrealized_pnl": 14168276.86,      "total_position_size": 2795.58898,      "total_position_size_long": 1807.5294,      "bias": 0.2931295787,      "label_basis": "point_in_time",      "grading": null    },    {      "ts": "2026-10-27T08:00:00+00:00",      "cohort": "4x whale",      "position_count": 23,      "position_count_long": 7,      "total_position_value": 1148971066.0,      "total_position_value_long": 299154299.6,      "total_unrealized_pnl": -10655654.18,      "total_position_size": 14400.31717,      "total_position_size_long": 3749.36926,      "bias": -0.479265739,      "label_basis": "point_in_time",      "grading": null    },    {      "ts": "2026-10-27T08:00:00+00:00",      "cohort": "3x whale",      "position_count": 25,      "position_count_long": 11,      "total_position_value": 219688224.9,      "total_position_value_long": 144653788.7,      "total_unrealized_pnl": 14249543.26,      "total_position_size": 2753.40277,      "total_position_size_long": 1812.97902,      "bias": 0.3169007017,      "label_basis": "point_in_time",      "grading": null    },    {      "ts": "2026-10-27T07:00:00+00:00",      "cohort": "4x whale",      "position_count": 23,      "position_count_long": 7,      "total_position_value": 1151367647.0,      "total_position_value_long": 302788216.6,      "total_unrealized_pnl": -11373018.52,      "total_position_size": 14411.56922,      "total_position_size_long": 3789.97391,      "bias": -0.4740373026,      "label_basis": "point_in_time",      "grading": null    },    {      "ts": "2026-10-27T07:00:00+00:00",      "cohort": "3x whale",      "position_count": 25,      "position_count_long": 12,      "total_position_value": 214136216.9,      "total_position_value_long": 145889764.5,      "total_unrealized_pnl": 14361118.53,      "total_position_size": 2680.3245,      "total_position_size_long": 1826.08956,      "bias": 0.3625884179,      "label_basis": "point_in_time",      "grading": null    }  ],  "hours": 25,  "nextCursor": "eyJtIjoiY29ob3J0X21ldHJpY3MiLCJ0IjoiMjAyNi0xMC0yNiAwOTowMDowMCJ9",  "grading": null,  "freshness": {    "newest_ts": "2026-10-27T09:00:00+00:00",    "behind_hours": 1,    "settled_lag_ceiling_hours": 3,    "stale": false  }}