Research notebook in Python

A reproducible study: what happened after the hours with the largest liquidations.

Cheapest plan
Free
Time to build
30 min
Endpoints
3
/liquidations/summary · /candles/{coin} · /liquidations
Languages
2
Node 18+, Python 3.9+, no installs

What you get

A notebook-style script (run it in Jupyter, VS Code or a terminal) that joins the hourly liquidation series of a coin with its candles and compares the returns 1, 4 and 24 hours after flush hours with all hours. Standard library only; one line turns the table into a pandas DataFrame.

note: The hourly series starts at 2026-10-23T00:00:00.000Z: earlier hours were not collected by every venue (coveredSince). Totals cover the whole window.
BTC, 123 hours since 2026-10-23, flush = top 5 % of hours
group                          hours   mean 1h  up 1h   mean 4h  up 4h  mean 24h up 24h
all hours                        123    -0.00%    46%    -0.02%    48%    -0.20%    36%
long flush (>= $4.9M)              7     0.19%    57%     0.35%    71%    -0.11%    57%
short flush (>= $2.0M)             7    -0.02%    57%     0.07%    67%     0.02%    75%
Data by Blockmetric · Source: Hyperliquid · Binance · Bybit

Real output of the program below, run against our API on 2026-10-28 10:38 UTC.

Build it step by step

  1. Open the script as a notebook

    Every "# %%" line starts a cell in Jupyter and VS Code. Or run it as python research.py.

  2. Fetch the hourly series

    GET /liquidations/summary?coin=BTC&hours=720 has data.series: long and short USD per hour. Hours that were not collected are null; the script leaves them out.

    curl -s -H "Authorization: Bearer $BLOCKMETRIC_API_KEY" \
      "$BLOCKMETRIC_BASE_URL/liquidations/summary?coin=BTC&hours=720"
  3. Join the candles

    Hourly closes of the same window, matched on the hour, give the forward returns after 1, 4 and 24 hours.

  4. Compare groups

    Flush hours are the top 5 % by long or short liquidations (PERCENTILE=95). The table shows the mean return and the share of hours that went up.

  5. Save and continue

    CSV=table.csv writes the joined table; pd.DataFrame(rows) gives you pandas.

The complete study

Set this once in the shell that runs the code:

export BLOCKMETRIC_API_KEY=bm_live_…   # create one on /account/api
export BLOCKMETRIC_BASE_URL=https://api.blockmetric.io/v1
"""Research notebook: do hours with unusually large liquidations ("flushes") mark turning points?

Python 3.9+, standard library only (pandas optional, see the last cell). Run it as a script, or open it in Jupyter or
VS Code: every "# %%" line starts a notebook cell. Joins the hourly liquidation series (GET /liquidations/summary)
with 1h candles (GET /candles/{coin}) and compares the forward returns after flush hours with all hours.

    export BLOCKMETRIC_API_KEY=bm_live_...            # free key: 30 days of history; Pro or API Dev: 1 year
    export BLOCKMETRIC_BASE_URL=https://<domain>/api/v1
    python research.py

Options: COIN (BTC), DAYS (30), PERCENTILE (95), CSV (write the joined table, optional).
Descriptive statistics over a short window, not a trading signal.
"""
# %% Setup
import csv
import json
import os
import statistics
import urllib.error
import urllib.parse
import urllib.request

BASE = os.environ.get("BLOCKMETRIC_BASE_URL", "").rstrip("/")
KEY = os.environ.get("BLOCKMETRIC_API_KEY")
COIN = os.environ.get("COIN", "BTC")
DAYS = int(os.environ.get("DAYS", "30"))
PERCENTILE = float(os.environ.get("PERCENTILE", "95"))


def api(path, **params):
    query = urllib.parse.urlencode({k: v for k, v in params.items() if v not in (None, "")})
    headers = {"Authorization": f"Bearer {KEY}"} if KEY else {"X-Blockmetric-Access": "public"}
    try:
        with urllib.request.urlopen(urllib.request.Request(f"{BASE}{path}?{query}", headers=headers), timeout=60) as res:
            return json.load(res)
    except urllib.error.HTTPError as e:
        err = json.load(e).get("error", {})
        raise SystemExit(f"{e.code} {err.get('code')}: {err.get('message')}")


# %% Fetch: hourly liquidations and hourly candles of the same window
summary = api("/liquidations/summary", coin=COIN, hours=DAYS * 24)
series = summary["data"]["series"]  # [{t, longUsd, shortUsd}], starts at coveredSince
candles = api(f"/candles/{urllib.parse.quote(COIN)}", tf="1h", limit=5000, **{"from": series[0]["t"]})["data"]["candles"]
close = {c["t"]: c["close"] for c in candles}
for w in summary["meta"].get("warnings") or []:
    print("note:", w.get("message"))

# %% Join and compute forward returns after 1, 4 and 24 hours
HOURS = [1, 4, 24]
rows = []
for i, s in enumerate(series):
    if s["t"] not in close or s["longUsd"] is None or s["shortUsd"] is None:  # null = an hour that was not collected
        continue
    row = {"t": s["t"], "long_usd": s["longUsd"], "short_usd": s["shortUsd"], "close": close[s["t"]]}
    for h in HOURS:
        later = series[i + h]["t"] if i + h < len(series) else None
        row[f"ret_{h}h"] = (close[later] / close[s["t"]] - 1) if later in close else None
    rows.append(row)


def threshold(values, pct):
    v = sorted(values)
    return v[min(len(v) - 1, int(len(v) * pct / 100))] if v else 0


long_cut = threshold([r["long_usd"] for r in rows], PERCENTILE)
short_cut = threshold([r["short_usd"] for r in rows], PERCENTILE)

# %% Compare: mean return and share of hours that went up afterwards
groups = {
    "all hours": rows,
    f"long flush (>= ${long_cut / 1e6:.1f}M)": [r for r in rows if long_cut > 0 and r["long_usd"] >= long_cut],
    f"short flush (>= ${short_cut / 1e6:.1f}M)": [r for r in rows if short_cut > 0 and r["short_usd"] >= short_cut],
}
print(f"{COIN}, {len(rows)} hours since {rows[0]['t'][:10] if rows else '-'}, flush = top {100 - PERCENTILE:g} % of hours")
print(f"{'group':<30} {'hours':>5}" + "".join(f" {'mean ' + str(h) + 'h':>9} {'up ' + str(h) + 'h':>6}" for h in HOURS))
for name, g in groups.items():
    cells = []
    for h in HOURS:
        r = [x[f"ret_{h}h"] for x in g if x[f"ret_{h}h"] is not None]
        cells.append(f" {statistics.mean(r) * 100:>8.2f}% {sum(1 for x in r if x > 0) / len(r) * 100:>5.0f}%" if r else f" {'-':>9} {'-':>6}")
    print(f"{name:<30} {len(g):>5}" + "".join(cells))
print(summary["meta"]["attribution"]["text"])

# %% Optional: save the table, or continue in pandas
if os.environ.get("CSV"):
    with open(os.environ["CSV"], "w", newline="") as f:
        w = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
        w.writeheader()
        w.writerows(rows)
    print("wrote", os.environ["CSV"])
# import pandas as pd
# df = pd.DataFrame(rows).set_index("t")
# df[["long_usd", "short_usd"]].plot()

Run it:

python research.py
node research.mjs
COIN=ETH DAYS=14 CSV=eth.csv python research.py

Download: research.py · research.mjs · requests.sh. MIT-0: use it as you like.

Real responses

What the API answered when we ran this recipe, with long lists shortened.

Hourly liquidation series: GET /liquidations/summary?coin=BTC&hours=24
{
  "data": {
    "hours": 24,
    "totalUsd": 30840084.18,
    "longUsd": 980227.16,
    "shortUsd": 29859857.02,
    "count": 1171,
    "byVenue": [
      {
        "venue": "binance",
        "longUsd": 581097.57,
        "shortUsd": 13817801.31,
        "count": 537
      },
      {
        "venue": "hyperliquid",
        "longUsd": 0,
        "shortUsd": 10671473.44,
        "count": 14
      },
      {
        "venue": "bybit",
        "longUsd": 399129.59,
        "shortUsd": 5370582.27,
        "count": 620
      }
    ],
    "byCoin": [
      {
        "coin": "BTC",
        "longUsd": 980227.16,
        "shortUsd": 29859857.02,
        "count": 1171
      }
    ],
    "largest": {
      "id": 303747,
      "ts": "2026-10-28T08:35:53.154Z",
      "venue": "hyperliquid",
      "coin": "BTC",
      "side": "short",
      "price": 81699.8005324,
      "qty": 77.6887,
      "usd": 6347151.29
    },
    "series": [
      {
        "t": "2026-10-27T10:00:00.000Z",
        "longUsd": 0,
        "shortUsd": 286060.78
      },
      {
        "t": "2026-10-27T11:00:00.000Z",
        "longUsd": 7264.51,
        "shortUsd": 881.06
      },
      {
        "t": "2026-10-27T12:00:00.000Z",
        "longUsd": 319.27,
        "shortUsd": 1191078.25
      },
      "… 22 more"
    ],
    "coveredSince": "2026-10-23T00:27:23.214Z",
    "updatedAt": "2026-10-28T09:51:34.179Z",
    "hourlySumsBefore": null
  },
  "meta": {
    "updatedAt": "2026-10-28T09:51:34.179Z",
    "sources": [
      "hyperliquid",
      "binance",
      "bybit"
    ],
    "plan": "api_dev",
    "delayMinutes": 0,
    "attribution": {
      "text": "Data by Blockmetric · Source: Hyperliquid · Binance · Bybit",
      "url": "https://blockmetric.io/"
    },
    "restricted": [
      "binance",
      "bybit"
    ],
    "license": {
      "id": "internal",
      "attributionRequired": false
    }
  }
}

Rate limits and pagination

  • Two requests per study, whatever the window.
  • The hourly series reaches back up to 1,095 days (as far as it is stored); single liquidations 120 days, those of $10,000 or more 3 years.
  • meta.warnings explains every cut the API made (here: hours before every venue was collected).
  • For single liquidations, page back with meta.nextCursor until it is null; up to 1,000 rows per page.

Common pitfalls

  • Overlapping windows: 24-hour returns of neighbouring hours share 23 hours, so do not treat them as independent samples.
  • A short window gives few flush hours; read the counts before the means.

Who it is for

  • Analysts who work in notebooks
  • Writers who need numbers they can reproduce
  • Quant researchers starting a new idea

Plan and limits

FreeCheapest
30 days of hourly liquidations and candles.
10/min, 1,000/day, 30 days of history
Pro$29/mo
365 days, personal research.
20/min, 5,000/day, 365 days of history
API Dev$49/mo
A year of history for research inside your organisation.
60/min, 50,000/day, 365 days of history

Compare plans

Honest limits

  • Totals and the hourly series include every venue we collect, as our own aggregate; single Binance and Bybit liquidations are not served while their licence review is pending.
  • Exchange totals are what the venues report and undercount.
  • Descriptive statistics over a short sample, not a trading signal.

Example projects

python-research

A small, reproducible study as a plain script: do hours with unusually large liquidations ("flushes") mark local turning points? research.py joins the hourly liquidation series of a coin (GET /liquidations/summary) with its 1h candles (GET /candles/{coin}), computes forward returns after 1, 4 and 24 hours and compares flush hours with all hours. Standard library only (csv, statistics) plus the blockmetric SDK. It runs in a terminal, a cron job or CI.

README.md · research.py

python-csv-export

Writes every liquidation of the last N hours that the API serves to a CSV file, following the API's cursor pages. Standard library + the blockmetric SDK only. Which venues are included depends on the API's data licence (meta.sources names them; single liquidations of a venue the API may not redistribute answer source_restricted).

README.md · export_liquidations.py

These projects use the official SDKs, which are being prepared for release. Until then the dependency-free program above runs as it is.

Next steps

More recipes like this

LiveHyperliquid markets 10sOperationalHyperliquid positions & whales 10sOperationalLiquidation heatmap & levels 12sOperationalLighter markets & liquidations 18sOperationalLighter positions & heatmap 10sOperationalLighter RH positions & heatmap 14sOperationalBinance markets 39sOperationalBybit markets 9sOperationalBinance liquidations 18sOperationalBybit liquidations 18sOperationaldYdX positions & heatmap 11sOperationalGMX positions & heatmap 14sOperationalLong/short & open interest history 2mOperationalOrder books & trade flow 9sOperationalOn-chain transfers 10sOperationalSecurity monitor 48sOperationalBlockmetric Sentiment 9mOperationalOfficial news 8mOperationalAlert delivery 11sOperational10:06:36 UTCData: Blockmetric Research · Binance · Bybit