Intro to Aperiodic Derivatives Metrics¶
This notebook introduces a simple derivatives regime dashboard using Aperiodic's
funding, open_interest, and basis metrics for Binance BTC perpetuals over
September 1, 2025 → February 28, 2026 at 5-minute frequency.
Why derivatives metrics matter¶
Derivatives data helps answer a different question than trade flow: how crowded is positioning, and how expensive is leverage?
- Funding tracks the periodic transfer that keeps perpetuals aligned with spot.
- Open interest helps show whether leverage is building or being unwound.
- Basis measures the futures/perpetual premium or discount relative to a reference price.
Together, these metrics are useful for spotting crowded bullish or bearish regimes, squeezes, and leverage resets.
Background reading
- Aperiodic derivatives market data: https://aperiodic.io/metrics/derivatives_market_data
- Binance Academy, funding rates: https://www.binance.com/en/academy/articles/what-are-funding-rates-in-crypto-markets
- CME Group, futures basis overview: https://www.cmegroup.com/education/courses/introduction-to-basis.html
In [1]:
from __future__ import annotations
import os
from datetime import date
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from aperiodic import get_derivative_metrics, get_ohlcv
sns.set_theme(style="whitegrid", context="talk", palette="deep")
pd.options.display.float_format = "{:,.4f}".format
plt.rcParams["figure.figsize"] = (14, 6)
plt.rcParams["figure.dpi"] = 140
plt.rcParams["savefig.dpi"] = 140
plt.rcParams["axes.spines.top"] = False
plt.rcParams["axes.spines.right"] = False
EXCHANGE = "binance-futures"
SYMBOL = "perpetual-BTC-USDT:USDT"
INTERVAL = "5m"
START_DATE = date(2025, 9, 1)
END_DATE = date(2026, 2, 28)
START_TS = pd.Timestamp(START_DATE)
END_TS = pd.Timestamp(END_DATE) + pd.Timedelta(days=1)
API_KEY = "..." # Set via APERIODIC_API_KEY env var or .env file
if API_KEY == "...":
API_KEY = os.getenv("APERIODIC_API_KEY", "...")
if API_KEY == "...":
raise RuntimeError("Set APERIODIC_API_KEY in the environment or in .env.")
def clip_window(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
out["time"] = pd.to_datetime(out["time"])
out = out.loc[(out["time"] >= START_TS) & (out["time"] < END_TS)]
return out.sort_values("time").reset_index(drop=True)
def format_time_axis(ax):
locator = mdates.AutoDateLocator(minticks=6, maxticks=10)
formatter = mdates.ConciseDateFormatter(locator)
ax.xaxis.set_major_locator(locator)
ax.xaxis.set_major_formatter(formatter)
def to_bps(series: pd.Series) -> pd.Series:
return series * 10_000
Fetch price, funding, open interest, and basis¶
In [2]:
price = clip_window(
get_ohlcv(
api_key=API_KEY,
timestamp="true",
interval=INTERVAL,
exchange=EXCHANGE,
symbol=SYMBOL,
start_date=START_DATE,
end_date=END_DATE,
output="pandas",
show_progress=False,
)
)[["time", "close"]]
funding = clip_window(
get_derivative_metrics(
api_key=API_KEY,
metric="funding",
timestamp="true",
interval=INTERVAL,
exchange=EXCHANGE,
symbol=SYMBOL,
start_date=START_DATE,
end_date=END_DATE,
output="pandas",
show_progress=False,
)
)
oi = clip_window(
get_derivative_metrics(
api_key=API_KEY,
metric="open_interest",
timestamp="true",
interval=INTERVAL,
exchange=EXCHANGE,
symbol=SYMBOL,
start_date=START_DATE,
end_date=END_DATE,
output="pandas",
show_progress=False,
)
)
basis = clip_window(
get_derivative_metrics(
api_key=API_KEY,
metric="basis",
timestamp="true",
interval=INTERVAL,
exchange=EXCHANGE,
symbol=SYMBOL,
start_date=START_DATE,
end_date=END_DATE,
output="pandas",
show_progress=False,
)
)
In [3]:
derivs = (
price.merge(funding, on="time", how="inner")
.merge(oi, on="time", how="inner")
.merge(basis, on="time", how="inner")
)
derivs["price_return_bps_1h"] = derivs["close"].pct_change(12) * 10_000
derivs["funding_rate_bps"] = derivs["funding_rate"] * 10_000
for col in ["funding_rate_bps", "basis_bps", "open_interest"]:
derivs[f"{col}_z_1d"] = (derivs[col] - derivs[col].rolling(288).mean()) / derivs[col].rolling(288).std()
derivs["oi_change_1h_pct"] = derivs["open_interest"].pct_change(12) * 100
derivs["next_1h_return_bps"] = derivs["close"].pct_change(12).shift(-12) * 10_000
derivs["stress_regime"] = (
(derivs["funding_rate_bps_z_1d"] > 1.5)
& (derivs["basis_bps_z_1d"] > 1.5)
& (derivs["open_interest_z_1d"] > 1.0)
)
derivs["crowding_score"] = derivs[["funding_rate_bps_z_1d", "basis_bps_z_1d", "open_interest_z_1d"]].mean(axis=1)
summary = pd.Series(
{
"Rows": len(derivs),
"Start": derivs["time"].min(),
"End": derivs["time"].max(),
"Mean funding (bps)": derivs["funding_rate_bps"].mean(),
"Mean basis (bps)": derivs["basis_bps"].mean(),
"Median open interest": derivs["open_interest"].median(),
"Stress regime share": derivs["stress_regime"].mean(),
}
)
summary
Out[3]:
Rows 52128 Start 2025-09-01 00:00:00 End 2026-02-28 23:55:00 Mean funding (bps) 0.3911 Mean basis (bps) -4.2997 Median open interest 89,319.8745 Stress regime share 0.0053 dtype: object
Charts 1-2 — BTC price and funding rate¶
In [4]:
fig, (ax_price, ax_funding) = plt.subplots(2, 1, figsize=(14, 10), sharex=True)
ax_price.plot(derivs["time"], derivs["close"], color="#2563eb", linewidth=1.2)
ax_price.set_title("BTC perpetual close price")
ax_price.set_ylabel("Price (USDT)")
format_time_axis(ax_price)
ax_funding.plot(derivs["time"], derivs["funding_rate_bps"], color="#dc2626", linewidth=1)
ax_funding.axhline(0, color="black", linestyle="--", linewidth=0.8)
ax_funding.set_title("Funding rate (bps)")
ax_funding.set_ylabel("bps")
format_time_axis(ax_funding)
plt.tight_layout()
Chart 3 — Funding distribution¶
In [5]:
fig, ax = plt.subplots()
ax.hist(derivs["funding_rate_bps"].dropna(), bins=80, color="#f97316", alpha=0.8, edgecolor="white")
ax.axvline(0, color="black", linestyle="--", linewidth=1)
ax.set_title("Distribution of 5-minute funding readings")
ax.set_xlabel("Funding rate (bps)")
plt.tight_layout()
Chart 4 — Price vs open interest¶
A rising market with rising open interest often suggests leverage is building alongside the move.
In [6]:
fig, ax1 = plt.subplots(figsize=(14, 6))
ax1.plot(derivs["time"], derivs["close"], color="#2563eb", linewidth=1.1, label="Close")
ax1.set_ylabel("Price (USDT)", color="#2563eb")
ax2 = ax1.twinx()
ax2.plot(derivs["time"], derivs["open_interest"], color="#059669", linewidth=1, alpha=0.8, label="Open interest")
ax2.set_ylabel("Open interest", color="#059669")
ax1.set_title("Price and open interest")
format_time_axis(ax)
plt.tight_layout()
Chart 5 — Basis in basis points¶
In [7]:
fig, ax = plt.subplots()
ax.plot(derivs["time"], derivs["basis_bps"], color="#7c3aed", linewidth=1)
ax.axhline(0, color="black", linestyle="--", linewidth=0.8)
ax.set_title("Basis (bps)")
ax.set_ylabel("bps")
format_time_axis(ax)
plt.tight_layout()
Chart 6 — Crowding score and highlighted stress regimes¶
In [8]:
fig, ax = plt.subplots(figsize=(14, 6))
ax.plot(derivs["time"], derivs["crowding_score"], color="#111827", linewidth=1.2)
ax.fill_between(
derivs["time"],
derivs["crowding_score"],
where=derivs["stress_regime"],
color="#ef4444",
alpha=0.2,
label="Stress regime",
)
ax.axhline(0, color="black", linestyle="--", linewidth=0.8)
ax.set_title("Composite derivatives crowding score")
ax.set_ylabel("Average z-score")
ax.legend(frameon=True)
format_time_axis(ax)
plt.tight_layout()
Chart 7 — Funding by weekday and hour¶
In [9]:
seasonality = derivs.assign(
weekday=derivs["time"].dt.day_name().str[:3],
hour=derivs["time"].dt.hour,
).pivot_table(index="weekday", columns="hour", values="funding_rate_bps", aggfunc="mean")
seasonality = seasonality.reindex(["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"])
fig, ax = plt.subplots(figsize=(16, 5))
sns.heatmap(seasonality, cmap="coolwarm", center=0, ax=ax)
ax.set_title("Average funding rate by weekday and hour (bps)")
ax.set_xlabel("Hour of day")
ax.set_ylabel("")
plt.tight_layout()
Takeaways¶
- Funding, basis, and open interest are complementary measures of leverage, crowding, and carry.
- Persistent positive funding plus rich basis often signals an increasingly expensive long trade.
- Rising open interest during a price move can indicate fresh positioning rather than mere price drift.
- The most useful regime read usually comes from the combination of these metrics, not any one series in isolation.
Further reading¶
- Aperiodic derivatives market data: https://aperiodic.io/metrics/derivatives_market_data
- Funding rates: https://www.binance.com/en/academy/articles/what-are-funding-rates-in-crypto-markets
- Basis overview: https://www.cmegroup.com/education/courses/introduction-to-basis.html