← Blog
Tutorial

Forecasting Hyperliquid funding: a 10-minute tutorial

Pull hourly Hyperliquid funding with the Python SDK, measure how sticky it is, and build the innovation series that actually carries the forecast. Requires a Pro API key.

Perpetual funding on Hyperliquid looks easy to forecast. It's sticky: today's hourly rate is a good guess for tomorrow's. But "tomorrow ≈ today" is a base rate, not a forecast — anyone can read it off the last price. The real question is what's left after you subtract that stickiness.

This is a follow-along tutorial. You'll need the Python SDK and a Pro API key (funding is a Pro dataset). By the end you'll have pulled real hourly funding, measured exactly how sticky it is, and built the innovation series — funding minus its own trailing average — which is where any genuine forecast has to live.

Set up

Install the SDK and export your key:

pip install "tessera-api[polars]"
export TESSERA_API_KEY="..."   # from https://tesseralytics.dev

Funding is a Pro dataset — a free key raises tessera.ForbiddenError on the read below. Get a Pro key from pricing.

Pull hourly funding

gold_funding_1h is hourly funding, partitioned per (coin, month). Read BTC, ETH and SOL for six months, projecting just the columns we need:

import polars as pl
import tessera

client = tessera.TesseraClient()  # reads $TESSERA_API_KEY

funding = client.read(
    "gold_funding_1h",
    coin=["BTC", "ETH", "SOL"],
    month=tessera.MonthSpan("2026-01", "2026-06"),
    columns=["time", "coin", "funding_rate"],
)

# Autocorrelation of hourly funding at increasing lags, per coin.
lags = [1, 6, 12, 24, 72, 168]   # hours

def autocorr(s: pl.Series, h: int) -> float:
    # Pearson correlation of the series against itself, lagged by h rows.
    df = pl.DataFrame({"x": s.slice(0, s.len() - h), "y": s.slice(h)})
    return df.select(pl.corr("x", "y")).item()

rows = []
for coin in funding["coin"].unique().to_list():
    s = funding.filter(pl.col("coin") == coin).sort("time")["funding_rate"]
    rows.append({"coin": coin, **{f"{h}h": round(autocorr(s, h), 3) for h in lags}})

pl.DataFrame(rows)
Funding is sticky — autocorrelation of hourly funding against itself, averaged over BTC, ETH and SOL.

Close to 0.9 at a one-hour lag, fading to almost nothing within a week. That slow decay is the base rate: "tomorrow ≈ today".

Subtract the stickiness

Define the innovation as funding minus its own 7-day trailing mean (168 hourly bars). Persistence lives in the level; strip it off and see how much survives:

d = (
    funding.sort("coin", "time")
    .with_columns(
        pl.col("funding_rate")
        .rolling_mean(window_size=168, min_samples=1)
        .over("coin")
        .alias("trailing_7d")
    )
    .with_columns(
        (pl.col("funding_rate") - pl.col("trailing_7d")).alias("innovation")
    )
    .drop_nulls()
)

for name in ("funding_rate", "innovation"):
    s = d.filter(pl.col("coin") == "BTC").sort("time")[name]
    print(f"{name:>13}  24h: {autocorr(s, 24):.3f}   72h: {autocorr(s, 72):.3f}")

At the one-day lag the subtraction barely moves the number — 0.285 → 0.234: the innovation still carries a little momentum of its own. At three days the picture flips — the raw level still autocorrelates at 0.122, while the innovation is 0.001. Past a day or so, essentially all of the level's stickiness was the trailing average; strip it and nothing multi-day survives.

What that means

"Tomorrow ≈ today" is doing almost all of the work. Persistence is a free base rate anyone can compute; subtract it and the forecastable leftover is small — and short-lived, with the innovation's autocorrelation dead inside about three days. That near-zero number is the honest size of the surprise: a feature to stack on a model you already have, not a buy/sell button. Everything above runs on one dataset — the funding tile — pulled through the SDK.