vike
GuidesHyperliquid

Count liquidations around the biggest move

Find the largest BTC hourly candle of a day, then total the long and short liquidations that landed inside that hour.

An event study in two calls: /ohlcv gives the 24 hourly BTC candles of 2026-08-17, and /liquidations gives the forced closes inside the hour of the largest move.

Run
export VIKE_API_KEY=...
python hl-liquidations-around-move.py             # real run: needs the key
python hl-liquidations-around-move.py --dry-run   # prints the requests it would make, calls nothing

It prints the hour, its move, and the count and dollar size of long and short liquidations.

hl-liquidations-around-move.py
"""Find the biggest BTC hourly move of one day, then total the liquidations that landed in that hour.

Run:  VIKE_API_KEY=... python hl-liquidations-around-move.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-liquidations-around-move"
BASE = "https://data.vike.io"
DRY = "--dry-run" in sys.argv


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 iso(ms):
    return datetime.fromtimestamp(ms / 1000, timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def main():
    # end is exclusive on /ohlcv: this is the 24 hourly bars of 2026-08-17.
    day = call("/v1/hyperliquid/ohlcv", symbol="BTC", interval="1h", limit=24,
               start="2026-08-17T00:00:00Z", end="2026-08-18T00:00:00Z")
    move_ts = 1786924800000  # --dry-run has no response to read
    if not DRY:
        if not day["candles"]:
            fail("no BTC candles for the day")
        biggest = max(day["candles"], key=lambda c: abs(c["close"] / c["open"] - 1))
        move_ts = biggest["ts"]
        print(f"biggest hour: {iso(move_ts)}  {biggest['close'] / biggest['open'] - 1:+.2%}  ({biggest['open']:g} -> {biggest['close']:g})")

    # /liquidations is newest-first, its bounds are whole seconds and its end is inclusive; 5000 is the most
    # one page holds. The hour's own end is passed, and a fill stamped exactly on it is dropped below.
    hour_end = move_ts + 3_600_000
    liqs = call("/v1/hyperliquid/liquidations", coin="BTC", limit=5000, start=iso(move_ts), end=iso(hour_end))
    if DRY:
        return
    rows = [r for r in liqs["liquidations"] if datetime.fromisoformat(r["liq_ts"]).timestamp() * 1000 < hour_end]
    if not rows:
        fail("no BTC liquidations in the move's hour")
    if liqs["nextCursor"]:
        print("  (a full page: the totals below cover the newest 5000 rows of the hour only)")
    # `side` is the side of the position that was closed: "long" means longs were liquidated.
    for side in ("long", "short"):
        hit = [r for r in rows if r["side"] == side]
        print(f"  {side:5} liquidated: {len(hit):5} fills  ${sum(r['size_usd'] for r in hit):,.0f}")


if __name__ == "__main__":
    try:
        main()
    except Exception as e:  # a shape the recipe does not expect fails by name, not by traceback
        fail(f"{type(e).__name__}: {e}")

Details that decide whether the totals are right

  • side is the position that was closed. long means longs were liquidated, which happens when price falls.
  • The bounds are whole seconds and end is inclusive. The script asks for the hour's own end and drops a fill stamped exactly on it, so an hour does not count its neighbour's first instant.
  • Newest first, 5000 rows at most. A busy hour can fill a page. The script says so when nextCursor is set; to total the whole hour, follow the cursor as shown in Paging large reads.
  • Candles are fills-derived and end is exclusive. With start and end both given, limit=24 returns the oldest 24 bars of the window, which is the whole day here.

Cohort labels on old rows are not point-in-time

Each liquidation carries a pnl_cohort, with pnl_cohort_basis saying whether it is the label at that hour or today's. This recipe does not read it. If you add a split by cohort, check the basis first: rows before 2026-04-07 always carry a current label.

On this page