Research notebook in Python
A reproducible study: what happened after the hours with the largest liquidations.
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 · BybitReal output of the program below, run against our API on 2026-10-28 10:38 UTC.
Build it step by step
Open the script as a notebook
Every "# %%" line starts a cell in Jupyter and VS Code. Or run it as python research.py.
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"Join the candles
Hourly closes of the same window, matched on the hour, give the forward returns after 1, 4 and 24 hours.
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.
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.pyDownload: 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
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
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.
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).
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
/funding/spreads/funding+2Backtest liquidation clustersHit rates: how often price reached the big liquidation cluster near it within 24 hours.Free45 min to build/heatmap/{coin}/candles/{coin}