Lesson 3.7.1 · 70 min

Perpetuals: funding mechanics, mark price and liquidations

A perpetual has three prices: the index of the spot markets, the mark computed by the exchange and the last price of the order book. Funding comes from the first and the third; liquidation, from the second. This lesson rebuilds the three pieces on four synthetic exchanges and measures what happens in the hours when the last price breaks away from the mark.

Fig. — Funding of each 8-hour period against the perpetual's average premium on alfa (XBQ, 2020-2025), in basis points
Level 3 lessons
  1. 3.1 · Futures in depth

    1. 3.1.1 Main futures contracts
    2. 3.1.2 Futures curve: basis, cost of carry, contango and roll yield
    3. 3.1.3 Roll management, margins, SPAN and implied leverage
    4. 3.1.4 Diversified futures systems: Carver's approach
  2. 3.2 · Futures spreads

    1. 3.2.1 Calendar spreads, butterflies and condors
    2. 3.2.2 Intercommodity spreads: crack, spark and crush
    3. 3.2.3 Rate and index spreads: 2s10s, NOB, NQ/ES and Europe versus the U.S.
    4. 3.2.4 Hedge ratios: by notional, by DV01 and by volatility
    5. 3.2.5 Spread data and backtesting: listed, synthetic and legged
    6. 3.2.6 Seasonality of spreads: multi-year analysis, patterns and traps
  3. 3.3 · Advanced forex

    1. 3.3.1 FX microstructure: OTC, ECN, last look and sessions
    2. 3.3.2 Carry and crash risk, momentum and value in currencies; baskets
    3. 3.3.3 Currency risk in multi-currency portfolios; execution via OANDA, IBKR or MT5
  4. 3.4 · Option theory and pricing

    1. 3.4.1 Payoffs, put-call parity and arbitrage bounds
    2. 3.4.2 Binomial trees, Black-Scholes-Merton, Black-76 and Monte Carlo
    3. 3.4.3 Greeks: delta, gamma, vega, theta, rho, vanna, volga and charm
    4. 3.4.4 Implied volatility: Newton-Raphson, Brent, py_vollib and QuantLib
    5. 3.4.5 Smile, skew and term structure; volatility surface with SVI
    6. 3.4.6 Realized vs implied volatility; volatility risk premium; VIX and VSTOXX
    7. 3.4.7 Advanced models: Heston, SABR and local volatility
  5. 3.5 · Option structures in Python

    1. 3.5.1 Directional and vertical structures: call, put, covered call, collar and spreads
    2. 3.5.2 Volatility and range structures: straddles, strangles, condors and butterflies
    3. 3.5.3 Calendars, diagonals, ratios, backspreads, risk reversals and jade lizard
    4. 3.5.4 A custom tool: payoff, aggregate Greeks and scenarios
    5. 3.5.5 Management: adjustments, rolls, early closes, early assignment and pin risk
  6. 3.6 · Systematic volatility and options strategies

    1. 3.6.1 Systematic premium selling: put-write, strangles and their tail risk
    2. 3.6.2 Delta hedging, gamma scalping and dispersion
    3. 3.6.3 VIX and VSTOXX strategies: volatility futures and ETPs
    4. 3.6.4 Options backtesting: data, fills and event adjustments
  7. 3.7 · Crypto derivatives and specific strategies

    1. 3.7.1 Perpetuals: funding mechanics, mark price and liquidations
    2. 3.7.2 Funding arbitrage, basis with futures and cross-exchange arbitrage
    3. 3.7.3 Crypto options, on-chain data and DeFi
    4. 3.7.4 ccxt for trading on multiple exchanges
    5. 3.7.5 MiCA: the EU framework for crypto-assets and their service providers (CASPs)

Three prices for a contract that never expires

Lesson 1.2.6 introduced the perpetual: a future without expiry that is anchored to spot with funding payments every 8 hours. Lesson 2.3.7 measured its funding and the cash-and-carry that collects it. This one goes down a level, to the mechanics that decide how much is paid and when a position is forcibly closed, and for that it is necessary to separate three prices that look like one on a trading screen:

  • Index. A weighted average (or median) of the spot prices of several exchanges. It is not traded: the perpetual's exchange computes it from external sources.
  • Last price. That of the last trade in the perpetual's order book. It is the one seen on the candlestick chart.
  • Mark. A reference price that the exchange computes from the index and the perpetual's premium. It is used to value unrealized PnL and to decide liquidation.
alfa spotIndexboreal spotcenit spotPerp order bookLast pricePremium8 h fundingMarkLiquidationUnrealized PnL
Mermaid source
flowchart TB
  S1[alfa spot] --> IX[Index]
  S2[boreal spot] --> IX
  S3[cenit spot] --> IX
  LB[Perp order book] --> UL[Last price]
  LB --> PR[Premium]
  IX --> PR
  PR --> FU[8 h funding]
  IX --> MK[Mark]
  PR --> MK
  MK --> LQ[Liquidation]
  MK --> PN[Unrealized PnL]
IN [01]python
import numpy as np
import pandas as pd

fu = pd.read_parquet("cripto3_funding.parquet")
cd = pd.read_parquet("cripto3_diario.parquet")
ex = pd.read_csv("cripto3_exchanges.csv")
print(fu.columns.tolist())
print(fu.groupby("exchange")["ts"].agg(["min", "max", "size"]))
print(ex.loc[ex["evento"] != "ficha", ["ts_utc", "evento"]]
      .to_string(index=False))
OUT [01]Four exchanges, two assets
['ts', 'exchange', 'activo', 'precio_indice', 'precio_mark', 'ultimo', 'prima_media', 'tasa_funding', 'interes_abierto_usd']
                               min                       max   size
exchange
alfa     2020-01-01 08:00:00+00:00 2025-12-31 16:00:00+00:00  13150
boreal   2020-01-01 08:00:00+00:00 2025-12-31 16:00:00+00:00  13150
cenit    2020-01-01 08:00:00+00:00 2025-12-31 16:00:00+00:00  13150
dedalo   2020-01-01 08:00:00+00:00 2022-11-11 08:00:00+00:00   6272
          ts_utc                evento
2022-11-06 00:00               rumores
2022-11-08 12:00 retiradas_suspendidas
2022-11-11 14:00      cese_negociacion
2022-11-11 23:00               quiebra

cripto3_funding.parquet has one row per exchange, asset and 8-hour period (00:00, 08:00 and 16:00 UTC): the index, the mark and the last price at that instant, the period's average premium, the funding rate and the open interest. There are 13,150 rows per exchange (two assets over six years) and 6,272 on dedalo, which stops trading on November 11, 2022.

The index in the data is the median of the available spot prices; dedalo drops out of it when withdrawals are suspended. Binance documents a somewhat different index, a weighted average of several exchanges, but with the same underlying idea: if a source deviates more than 3% from the median of all of them, its price is clipped to 1.03 or 0.97 times the median, and a source that has not updated in five minutes gets a weight of zero1Note 1 · Binance, “What Are Mark Price and Price Index in USDS-Margined Futures?”: index as a weighted average of the spot prices of several exchanges, outliers clipped to ±3% of the median (±1% for some symbols) and zero weight for sources not updated in five minutes (link in the resources).. The daily close lets us check why it matters:

IN [02]python
x = cd[cd["activo"] == "XBQ"]
sp = x.pivot(index="fecha", columns="exchange",
             values="cierre_spot")
abiertas = x.pivot(index="fecha", columns="exchange",
                   values="retiradas_abiertas")
cuentan = sp.where(abiertas == True)       # dedalo drops out on suspension
indice = cuentan.median(axis=1)
media_4 = sp.mean(axis=1)                   # naive average, with dedalo
dias = ["2020-03-12", "2022-11-08", "2022-11-09", "2022-11-10",
        "2025-10-10"]
tabla = 100 * np.log(sp.loc[dias].div(indice[dias], axis=0))
tabla["media_4"] = 100 * np.log(media_4[dias] / indice[dias])
tabla.round(2)
OUT [02]The index: median of the spot prices that count
exchange    alfa  boreal  cenit  dedalo  media_4
fecha
2020-03-12 -0.03    0.03  -5.45    0.07    -1.32
2022-11-08  0.03   -0.01   0.00   -1.74    -0.42
2022-11-09 -0.00    0.01   0.00  -10.44    -2.51
2022-11-10  0.00   -0.03   0.04  -16.95    -3.97
2025-10-10 -3.96    0.00   0.08     NaN    -1.28

The table gives, in %, the distance of each spot price from the index at the close (23:00 UTC) of five days. On March 12, 2020 the cenit spot trades 5.45% below the others (a planted four-hour dislocation); on October 10, 2025, the alfa one, 3.96%. From November 8 to 10, 2022, dedalo sinks to a 16.95% discount with withdrawals suspended: its balances can no longer be taken out and its price stops being a market price. The median does not notice any of the three episodes. A simple average of the spot prices would have moved 1.32%, 3.97% and 1.28%: with an index like that, a dislocation on a single exchange would move the mark, the PnL and the liquidations of all the others.

Funding as the data computes it

The funding formula in the data is the one Binance publishes for its linear perpetuals, which lesson 1.2.6 rebuilt for a single coin:

F=P+clamp⁡(I−P, −0.05 %, 0.05 %)F = P + \operatorname{clamp}(I - P,\ -0.05\,\%,\ 0.05\,\%)

with I=0.01 %I = 0.01\,\% per 8-hour period (0.03% daily) and PP the period's average premium. If F>0F > 0 longs pay shorts; if it is negative, the other way around. What lesson 1.2.6 did not see is where PP comes from. Binance does not measure the premium with the last price, which can move with a single small trade, but with impact prices: the average price at which a fixed notional would execute against the order book, the impact margin notional, which is 200 USDT divided by the initial margin at maximum leverage2Note 2 · Binance, “Introduction to Binance Futures Funding Rates”: premium with impact prices, one sample every 5 seconds, time-weighted average of the period with increasing weights (1, 2, 3… n) over the 5,760 samples of 8 hours, default interval of 8 hours (00:00, 08:00 and 16:00 UTC), amount = notional (mark × size) × rate, and caps of ±0.75 times the maintenance rate on the main symbols (±2% on the rest). The caps in the data are specific to each fictitious exchange.. The premium at each instant is

Pt=max⁡(0, impact bid−index)−max⁡(0, index−impact ask)indexP_t = \frac{\max(0,\ \text{impact bid} - \text{index}) - \max(0,\ \text{index} - \text{impact ask})}{\text{index}}
IN [03]python
# example order book of the perpetual: (price, quantity in XBQ)
bids = [(64990, 0.15), (64985, 0.10), (64975, 0.20), (64960, 0.40)]
asks = [(65010, 0.12), (65020, 0.10), (65035, 0.25), (65050, 0.50)]
indice_ej = 64950.0
nocional_impacto = 200 / (1 / 125)   # 200 USDT / margin at 125x


def precio_impacto(niveles, nocional):
    """Average price of executing `nocional` USDT against the book."""
    resto, coste, cant = nocional, 0.0, 0.0
    for p, q in niveles:
        tomo = min(q, resto / p)
        coste, cant = coste + tomo * p, cant + tomo
        resto -= tomo * p
        if resto <= 1e-9:
            return coste / cant
    raise ValueError("the order book does not cover the impact notional")


ib = precio_impacto(bids, nocional_impacto)
ia = precio_impacto(asks, nocional_impacto)
prima = (max(0, ib - indice_ej) - max(0, indice_ej - ia)) / indice_ej
print(f"impact notional: {nocional_impacto:,.0f} USDT")
print(f"impact bid {ib:,.2f}  impact ask {ia:,.2f}")
print(f"premium: {1e4 * prima:.2f} bp")
OUT [03]A perpetual's premium with impact prices
impact notional: 25,000 USDT
impact bid 64,983.45  impact ask 65,023.30
premium: 5.15 bp

With a maximum leverage of 125x (initial margin of 0.8%), the impact notional is 25,000 USDT. The impact bid, what one would collect per unit selling that notional, is 64,983.45, and it sits above the index: the perpetual's entire order book trades rich and the premium is +5.15 basis points. If the index were between the impact bid and the impact ask, the premium would be zero, even if the last price moved: the entire order book has to shift, not a single trade.

The period's average is not simple either: recent samples weigh more, so that funding reacts sooner to a change in the premium at the end of the period.

IN [04]python
n = 5760                          # one sample every 5 s over 8 hours
p = np.full(n, 2e-4)              # 2 bp for 7 hours…
p[-n // 8:] = 12e-4               # …and 12 bp in the last hour
w = np.arange(1, n + 1)
print(f"simple average: {1e4 * p.mean():.2f} bp")
print(f"weighted average 1..n: {1e4 * (w * p).sum() / w.sum():.2f} bp")
OUT [04]Simple average versus average with increasing weights
simple average: 3.25 bp
weighted average 1..n: 4.34 bp

One hour of premium at 12 basis points after seven at 2 gives a simple average of 3.25 and a weighted one of 4.34. The difference can decide whether the period falls inside or outside the band in which funding is fixed. The course data simplifies here: prima_media is the simple average of the period's eight hourly premiums. With that column, the rule is checked on the four exchanges at once, each with its own cap:

IN [05]python
I, RECORTE = 0.0001, 0.0005
TOPE = {"alfa": 0.0075, "boreal": 0.005, "cenit": 0.00375,
        "dedalo": 0.005}
P = fu["prima_media"]
tope = fu["exchange"].map(TOPE)
calc = (P + (I - P).clip(-RECORTE, RECORTE)).clip(-tope, tope)
print("maximum difference:", (calc.round(6) - fu["tasa_funding"])
      .abs().max())
tasa = fu["tasa_funding"]
resumen = fu.assign(
    base=tasa == I, negativo=tasa < 0,
    en_tope=tasa.abs() >= tope - 1e-12,
).groupby(["exchange", "activo"]).agg(
    base_pct=("base", "mean"), neg_pct=("negativo", "mean"),
    minimo_pb=("tasa_funding", "min"),
    maximo_pb=("tasa_funding", "max"), en_tope=("en_tope", "sum"))
resumen[["base_pct", "neg_pct"]] *= 100
resumen[["minimo_pb", "maximo_pb"]] *= 1e4
resumen.round(1)
OUT [05]The published funding, rebuilt on the four exchanges
maximum difference: 0.0
                 base_pct  neg_pct  minimo_pb  maximo_pb  en_tope
exchange activo
alfa     XBQ         77.6      5.6       -5.8       20.2        0
         XEQ         69.3      7.4       -6.5       29.2        0
boreal   XBQ         78.0      4.1       -5.2       22.2        0
         XEQ         70.7      5.7       -6.2       29.6        0
cenit    XBQ         75.8      8.2       -7.7       21.7        0
         XEQ         68.8      8.9       -7.5       28.5        0
dedalo   XBQ         70.3      3.9      -50.0       20.6        1
         XEQ         56.9      5.1      -50.0       29.8        2

All 45,722 rates match the formula without error. The figure at the top draws the rule with the XBQ periods on alfa: while the average premium is between −4 and +6 basis points, the clamp does not act and P+(I−P)P + (I - P) is exactly II; outside the band, funding is the premium minus 5 basis points above, or plus 5 below. 77.6% of XBQ periods and 69.3% of XEQ ones pay that exact basis point. 5.6% and 7.4% are negative. cenit has more negative periods (8.2% and 8.9%) because its perpetual trades, on average, slightly cheaper.

The caps only act on dedalo: three periods after the suspension of withdrawals, with the premium collapsed, pay the minimum of −0.50%, −50 basis points in eight hours (lesson 3.7.2). On the other exchanges funding does not get close to its cap; the maximum, between 20 and 30 basis points, comes in the weeks of euphoria.

IN [06]python
alfa = fu[fu["exchange"] == "alfa"]
pagado = (100 * alfa.groupby(["activo", alfa["ts"].dt.year])
          ["tasa_funding"].sum()).unstack().round(1)
print("funding paid by a long, % of notional:")
print(pagado.to_string())
xbq = alfa[alfa["activo"] == "XBQ"].set_index("ts")
print(f"XBQ funding skewness: {xbq['tasa_funding'].skew():.1f}")
OUT [06]What a perpetual long pays each year
funding paid by a long, % of notional:
ts      2020  2021  2022  2023  2024  2025
activo
XBQ     25.6  24.4  10.4  11.8  11.2   9.7
XEQ     52.9  38.3  11.7  12.4  10.9   9.4
XBQ funding skewness: 3.7

An XBQ long on the alfa perpetual paid 25.6% of its notional in 2020 and 24.4% in 2021, and between 9.7% and 11.8% in the following years, close to the floor of almost 11% set by the II term. On XEQ, 52.9% in 2020. The distribution is very skewed (3.7): almost always the band's basis point, and every now and then weeks of very high funding. That 11% floor is not a market price: it is a convention of the formula, and it is the reason why a leveraged long on a perpetual is expensive to hold for months (lesson 1.2.6) and why the cash-and-carry collects something even in calm markets (lesson 3.7.2).

Mark price: the price that decides

Binance defines the mark of its linear perpetuals as the median of three prices3Note 3 · Binance, “What Are Mark Price and Price Index in USDS-Margined Futures?”: mark = median(price 1, price 2, contract price); under extreme conditions it may use only price 2. The mark is used for liquidations and unrealized PnL; realized PnL, with executed prices.: the index adjusted by the last funding in proportion to the time left until the next payment (price 1), the index plus the 30-second moving average of the difference between the order book midpoint and the index (price 2), and the contract's last price. A median of three ignores the extreme, and the 30-second average dilutes what is short-lived:

IN [07]python
def mark_binance(indice, medio, ultimo, funding, horas_al_pago):
    """Median(P1, P2, last) with the 30 s average of the basis."""
    p1 = indice * (1 + funding * horas_al_pago / 8)
    base = pd.Series(medio - indice).rolling(30, min_periods=1).mean()
    p2 = indice + base.to_numpy()
    return np.median(np.vstack([p1, p2, ultimo]), axis=0)


ind = np.full(600, 100_000.0)                # 10 minutes, 1 per s
casos = [("5 s wick", 5), ("5 min dislocation", 300)]
for nombre, dura in casos:
    medio = ind * 1.0002
    medio[120:120 + dura] *= 0.96            # the order book falls 4%
    ultimo = medio.copy()
    mk = mark_binance(ind, medio, ultimo, 0.0001, 4)
    print(f"{nombre}: last {100 * (ultimo.min() / 1e5 - 1):+.2f}%,"
          f" mark {100 * (mk.min() / 1e5 - 1):+.2f}%")
OUT [07]Binance's mark facing a wick and a dislocation
5 s wick: last -3.98%, mark -0.65%
5 min dislocation: last -3.98%, mark -3.98%

With a five-second decline in the order book, the last price falls 3.98% and the mark, 0.65%: the 30-second average only picks up a sixth and the median discards the extreme. With a five-minute decline, the mark falls as much as the last price: the average has already absorbed it entirely and two of the three prices coincide. The mark protects against wicks, not against an order book that really shifts. Only the index, which comes from other exchanges, protects against that.

The data uses a simpler version: mark = index × (1 + smoothed premium), with each hour's premium averaged with the previous hour's. Since the mark starts from the index, a dislocation of an exchange's spot and perpetual moves its last price and not its mark:

IN [08]python
fu["mark_indice_pb"] = 1e4 * np.log(fu["precio_mark"]
                                    / fu["precio_indice"])
fu["ultimo_mark_pb"] = 1e4 * np.log(fu["ultimo"] / fu["precio_mark"])
vivos = fu[fu["exchange"] != "dedalo"]
print(vivos.groupby("exchange")[["mark_indice_pb", "ultimo_mark_pb"]]
      .agg(["std", "min", "max"]).round(1))
peores = vivos.nsmallest(2, "ultimo_mark_pb")
print(peores[["ts", "exchange", "activo", "ultimo_mark_pb",
              "mark_indice_pb"]].round({"ultimo_mark_pb": 1,
                                        "mark_indice_pb": 1})
      .to_string(index=False))
OUT [08]Mark, index and last price in the data
         mark_indice_pb             ultimo_mark_pb
                    std   min   max            std    min   max
exchange
alfa                5.8 -13.5  36.1            3.6  -19.2  16.9
boreal              5.8 -11.4  36.1            3.6  -16.7  17.4
cenit               5.8 -12.9  34.8            8.3 -660.8  16.5
                       ts exchange activo  ultimo_mark_pb  mark_indice_pb
2020-03-13 00:00:00+00:00    cenit    XEQ          -660.8            -4.5
2020-03-13 00:00:00+00:00    cenit    XBQ          -548.3            -5.3

On the three exchanges that do not go bankrupt, the mark deviates from the index with a standard deviation of 5.8 basis points and never more than 36: it is the perpetual's premium, the same one that drives funding. The last price deviates from the mark somewhat less (a standard deviation of 3.6 basis points on alfa and boreal), except on cenit, where the standard deviation rises to 8.3 because of a single instant: on March 13, 2020 at 00:00, in the middle of the dislocation, its XEQ last price is 660.8 basis points (6.6%) below the mark, and the mark, only 4.5 below the index. A 20x XEQ long on cenit would have seen on the chart a decline that liquidates its position; the engine, which looks at the mark, does not liquidate it because of that.

The liquidation engine

Binance describes the liquidation condition like this: the position is liquidated when its collateral, the initial margin plus realized and unrealized PnL (at the mark), falls below the maintenance margin4Note 4 · Binance, “Binance Futures Liquidation Protocols”: collateral = initial margin + realized PnL + unrealized PnL < maintenance margin; cancellation of open orders; an IOC order to reduce the shortfall; liquidation fee for Binance unless the position goes bankrupt; bankrupt positions pass to the insurance fund and, if it is not enough, to auto-deleveraging (ADL). Each exchange's maintenance rate also depends on the size of the position, in tiers; the lesson uses a fixed 0.5%.. For an isolated linear position of quantity qq opened at EE, with margin qE/Lq E / L and maintenance rate mm, the liquidation mark of a long comes from equating collateral and maintenance:

qEL+q (M−E)=m q M⇒Mliq=E 1−1/L1−m\frac{qE}{L} + q\,(M - E) = m\,q\,M \quad\Rightarrow\quad M_{liq} = E\,\frac{1 - 1/L}{1 - m}

and that of a short, E (1+1/L)/(1+m)E\,(1 + 1/L)/(1 + m). The bankruptcy price is the one at which the collateral reaches zero: E (1−1/L)E\,(1 - 1/L) for the long. Between the two lies the maintenance margin, which is what the engine has to close the position without losses for anyone.

IN [09]python
def precio_liquidacion(entrada, apal, mmr=0.005, lado="largo"):
    """Linear isolated margin: collateral = maintenance margin.
    No commissions or funding; mmr = maintenance rate."""
    if lado == "largo":
        return entrada * (1 - 1 / apal) / (1 - mmr)
    return entrada * (1 + 1 / apal) / (1 + mmr)


filas = []
for L in (3, 5, 10, 20, 50, 100):
    filas.append({
        "apal": L,
        "liquidacion_pct": 100 * (precio_liquidacion(1, L) - 1),
        "aprox_1.2.6_pct": 100 * (-1 / L + 0.005),
        "quiebra_pct": -100 / L,
    })
pd.DataFrame(filas).set_index("apal").round(2)
OUT [09]Liquidation and bankruptcy price of an isolated long
      liquidacion_pct  aprox_1.2.6_pct  quiebra_pct
apal
3              -33.00           -32.83       -33.33
5              -19.60           -19.50       -20.00
10              -9.55            -9.50       -10.00
20              -4.52            -4.50        -5.00
50              -1.51            -1.50        -2.00
100             -0.50            -0.50        -1.00

The approximation from lesson 1.2.6, E (1−1/L+m)E\,(1 - 1/L + m), is almost identical to the exact one: at 10x, −9.50% versus −9.55%. What the table teaches is how thin the margin is: at 10x bankruptcy is at −10% and liquidation half a point before; at 100x, liquidation comes with a decline of 0.50%, which XBQ does in many hours of the year. The liquidation order is executed in an order book that, at that moment, is usually falling. If it fills better than the bankruptcy price, the exchange collects its fee; if it fills worse, the difference is a loss someone has to pay:

yesnonoMark at P_liqCancels ordersIOC orderDoes it fill?Liquidation feeInsurance fundIs it enough?ADL
Mermaid source
flowchart TB
  A[Mark at P_liq] --> B[Cancels orders]
  B --> C[IOC order]
  C --> D{Does it fill?}
  D -- yes --> E[Liquidation fee]
  D -- no --> F[Insurance fund]
  F --> G{Is it enough?}
  G -- no --> H[ADL]

The insurance fund absorbs bankrupt positions. If it is not enough, ADL closes opposite positions of other clients, the ones that are winning: in a violent sell-off, someone who was short with profits can see their position closed without having asked. For a hedged strategy (lesson 3.7.2), ADL is a concrete risk: it leaves one leg without the other. With cross margin, by contrast, the whole account balance backs all positions: it holds up longer, but one bad position can take the capital of the others.

The wick hours

The last price and the mark separate above all in cascading sell-offs: liquidations sweep the order book, the last price prints a low far below the rest of the market for seconds and comes back. The 1-hour bars of the alfa perpetual carry both prices:

IN [10]python
h1 = pd.read_parquet("cripto3_1h.parquet")
b = h1[h1["activo"] == "XBQ"].set_index("ts")
ap_mark = b["mark_cierre"].shift(1)          # the opening mark
caida_ultimo = 100 * (b["minimo"] / b["apertura"] - 1)
caida_mark = 100 * (b["mark_minimo"] / ap_mark - 1)
mecha = 100 * np.log(b["mark_minimo"] / b["minimo"])
velas = pd.DataFrame({"caida_ultimo": caida_ultimo,
                      "caida_mark": caida_mark, "mecha": mecha})
print(f"hours with a last-price wick > 3%: {(mecha > 3).sum()}"
      f" of {len(b)}")
velas[velas["mecha"] > 3].round(1)
OUT [10]The wick hours
hours with a last-price wick > 3%: 14 of 52608
                           caida_ultimo  caida_mark  mecha
ts
2020-03-12 10:00:00+00:00         -13.6        -9.5    4.6
2020-03-12 23:00:00+00:00         -18.7       -14.8    4.7
2020-03-13 02:00:00+00:00         -15.3       -11.3    4.6
2021-05-19 12:00:00+00:00         -17.0       -13.1    4.6
2021-05-19 13:00:00+00:00         -11.9        -7.7    4.6
2022-05-09 20:00:00+00:00         -11.0        -6.8    4.7
2022-06-13 03:00:00+00:00         -12.7        -8.6    4.6
2022-11-08 15:00:00+00:00         -11.9        -7.7    4.6
2022-11-09 19:00:00+00:00         -13.6        -9.5    4.6
2024-08-05 01:00:00+00:00         -12.7        -8.6    4.5
2025-10-10 21:00:00+00:00         -15.4       -11.4    4.6
2025-10-10 22:00:00+00:00          -0.2        -0.2    4.0
2025-10-10 23:00:00+00:00          -0.1        -0.1    4.0
2025-10-11 00:00:00+00:00          -0.1        -0.1    4.0

Out of 52,608 hours, only 14 have the last price's low more than 3% below the mark's. Eleven are the planted candles: the mark falls between 6.8% and 14.8% within the hour and the last price, about 4 points more. The other three, from October 10, 2025 at 22:00 to the 11th at 00:00, are something else: the alfa dislocation, with its order book 4% below the index for hours, with hardly any movement within each hour. A wick lasts seconds; a dislocation, hours. It is the same difference that separated the two cases of Binance's mark.

Fig. — Hourly range of the last price and of the mark of alfa's XBQ perpetual on 8 November 2022, and liquidation price of a 10x long opened at 00:00

The figure follows November 8, 2022 hour by hour. A 10x long opened at 00:00 at 21,868.1 has its liquidation at 19,780 (the dashed line). At 15:00 the mark falls to 20,041 and does not touch it; the last price drops to 19,140 and crosses it. With the last price, the long would have been liquidated; with the mark, it stays alive. To measure how much this changes in general, an experiment: open a long every day at 00:00 UTC and hold it for 24 hours, with different leverage levels, and count on how many days it is liquidated depending on the price used.

IN [11]python
b = b.assign(dia=b.index.floor("D"))
g = b.groupby("dia")
entrada = g["apertura"].first()
min_mark, min_ultimo = g["mark_minimo"].min(), g["minimo"].min()
cierre_dia = g["cierre"].last()
filas, solo_ultimo = [], {}
for L in (5, 10, 20, 50):
    pl = precio_liquidacion(entrada, L)
    por_mark, por_ultimo = min_mark <= pl, min_ultimo <= pl
    filas.append({"apal": L, "por_mark": por_mark.sum(),
                  "por_ultimo": por_ultimo.sum(),
                  "con_cierre_diario": (cierre_dia <= pl).sum()})
    solo_ultimo[L] = por_ultimo & ~por_mark
print(f"{len(entrada)} one-day longs, opened at 00:00 UTC")
print(pd.DataFrame(filas).set_index("apal").to_string())
for L in (10, 20):
    print(f"{L}x, liquidated by the last price and not by the mark:",
          [d.strftime("%Y-%m-%d") for d in
           solo_ultimo[L][solo_ultimo[L]].index])
OUT [11]Liquidated depending on the price used
2192 one-day longs, opened at 00:00 UTC
      por_mark  por_ultimo  con_cierre_diario
apal
5            2           2                  1
10          13          15                  9
20         195         210                103
50        1069        1117                571
10x, liquidated by the last price and not by the mark: ['2022-11-08', '2024-08-05']
20x, liquidated by the last price and not by the mark: ['2021-06-23', '2021-07-06', '2021-11-24', '2022-01-06', '2022-03-15', '2022-07-16', '2022-07-23', '2022-08-23', '2022-11-09', '2023-01-09', '2024-01-11', '2024-03-02', '2025-02-09', '2025-02-18', '2025-04-03', '2025-06-19']

Three readings. First: leverage rules. At 5x, two one-day longs out of 2,192 are liquidated; at 10x, 13; at 20x, 195, one day in eleven; at 50x, almost half. Second: the last price would liquidate somewhat more than the mark (15 versus 13 at 10x), and at 10x the two days of difference are exactly two of the planted candles, November 8, 2022 and August 5, 2024. At 20x the list is longer and made of ordinary days: the mark in the data is smoother than the last price in any hour, not only in the wicks. Third, the most important for a backtest: with daily closes you see 9 of the 13 liquidations at 10x and 103 of the 195 at 20x. Days that fall and recover before the close liquidate all the same, and leave no trace at the close.

Aggregated liquidations: the cascade seen from outside

Public aggregators publish liquidations by hour, exchange and side, without the detail of each position. cripto3_liquidaciones.parquet mimics that format (only hours with at least 250,000 USD):

IN [12]python
lq = pd.read_parquet("cripto3_liquidaciones.parquet")
dia = lq.set_index("ts")["importe_usd"].resample("D").sum() / 1e6
print(f"daily median: {dia.median():,.0f} M USD")
print((dia.nlargest(5) / 1e3).round(2).to_string())
top = lq[lq["ts"].dt.floor("D") == dia.idxmax()]
print(top.groupby("lado")["importe_usd"].sum().div(1e9).round(2))
OUT [12]Aggregated liquidations: the cascade days
daily median: 63 M USD
ts
2025-10-10 00:00:00+00:00    3.08
2021-05-19 00:00:00+00:00    2.52
2025-02-13 00:00:00+00:00    1.57
2025-02-08 00:00:00+00:00    1.44
2022-05-09 00:00:00+00:00    1.28
lado
corto    0.13
largo    2.95
Name: importe_usd, dtype: float64

A normal day liquidates about 63 million USD across the four exchanges; the worst, October 10, 2025, 3,080 million, 96% of them longs (2.95 billion versus 0.13 of shorts). It is followed by May 19, 2021 and two days in February 2025. Cascades are asymmetric because positioning is: in the data, the leveraged majority is usually long, and the engine's rule liquidates more when the perpetual's premium is high (euphoria).

That rule is planted: the fraction of long open interest liquidated in an hour is min⁡(0.25, 1.5 (c−0.6 %)1.5×load)\min(0.25,\ 1.5\,(c - 0.6\,\%)^{1.5} \times \text{load}), with cc the mark's decline in the hour and a load that rises with the premium. Can it be recovered with what an outsider would see, the aggregated liquidations, the mark of the candles and the open interest every 8 hours?

IN [13]python
lar = lq[(lq["exchange"] == "alfa") & (lq["activo"] == "XBQ")
         & (lq["lado"] == "largo")].set_index("ts")["importe_usd"]
oi = (fu[(fu["exchange"] == "alfa") & (fu["activo"] == "XBQ")]
      .set_index("ts")[["interes_abierto_usd", "prima_media"]]
      .reindex(b.index, method="ffill"))
caida = np.log(ap_mark / b["mark_minimo"])
d = pd.DataFrame({"caida": caida, "liq": lar.reindex(b.index),
                  "oi": oi["interes_abierto_usd"],
                  "prima": oi["prima_media"]}).dropna(
                      subset=["caida", "oi"])
d["liq"] = d["liq"].fillna(0)
d["tramo"] = pd.cut(100 * d["caida"], [0, 0.4, 0.6, 1, 2, 4, 30])
print(d.groupby("tramo", observed=True).agg(
    horas=("liq", "size"),
    con_liq_pct=("liq", lambda s: 100 * (s > 0).mean()),
    fraccion_pb=("liq", "sum")).assign(
        fraccion_pb=lambda t: 1e4 * t["fraccion_pb"]
        / d.groupby("tramo", observed=True)["oi"].sum()).round(2))
s = d[(d["liq"] > 0) & (d["caida"] > 0.008) & (d["prima"] < I)]
pend, cte = np.polyfit(np.log(s["caida"] - 0.006),
                       np.log(s["liq"] / s["oi"]), 1)
print(f"exponent {pend:.2f}, constant {np.exp(cte):.2f}"
      f" ({len(s)} hours)")
OUT [13]The engine's rule, recovered from outside
             horas  con_liq_pct  fraccion_pb
tramo
(0.0, 0.4]   37590         0.00         0.00
(0.4, 0.6]    7695         0.00         0.00
(0.6, 1.0]    5191        62.72         1.12
(1.0, 2.0]    1836       100.00         9.90
(2.0, 4.0]     233       100.00        43.16
(4.0, 30.0]     33       100.00       210.21
exponent 1.47, constant 1.29 (2176 hours)

By bins of the mark's decline, the threshold appears on its own: in the 45,285 hours with declines of up to 0.6% there is not a single long liquidation on alfa. Between 0.6% and 1%, 62.7% of the hours have liquidations (the rest fall below the published minimum of 250,000 USD) and 1.1 basis points of open interest are liquidated; with declines of more than 4%, 2.1%. A log regression on the hours with liquidations, declines of more than 0.8% and a low premium (load close to 1) gives an exponent of 1.47, versus the planted 1.5, and a constant of 1.29 versus 1.5. The constant comes out low mainly because of a detail of the data: the published open interest is that of the last 8-hour fixing, before the wave and therefore larger than that of the hour; the ratio ends up below the real one.

Fig. — Fraction of long open interest liquidated in each hour against the mark drop within that hour (alfa, XBQ, 2020-2025), on log scales

The figure shows the cloud on a log scale with the planted rule dashed: nothing to the left of the threshold and a fraction that grows faster than the decline. That convexity is the cascade: each liquidation sells into an order book that is already falling, which pushes the mark a little lower and reaches the liquidation prices of the next ones. Open interest records it:

IN [14]python
a8 = (fu[(fu["exchange"] == "alfa") & (fu["activo"] == "XBQ")]
      .set_index("ts"))
tramo = a8.loc["2021-05-19 08:00":"2021-05-21 16:00"]
pd.DataFrame({
    "oi_millones_usd": tramo["interes_abierto_usd"] / 1e9 * 1e3,
    "indice": tramo["precio_indice"],
    "oi_miles_xbq": tramo["interes_abierto_usd"]
    / tramo["precio_indice"] / 1e3,
}).round(1)
OUT [14]Open interest after a wave
                           oi_millones_usd   indice  oi_miles_xbq
ts
2021-05-19 08:00:00+00:00           9555.1  53271.8         179.4
2021-05-19 16:00:00+00:00           7096.1  42586.7         166.6
2021-05-20 00:00:00+00:00           7011.7  43130.2         162.6
2021-05-20 08:00:00+00:00           7223.7  43444.3         166.3
2021-05-20 16:00:00+00:00           7159.9  43973.2         162.8
2021-05-21 00:00:00+00:00           7342.9  44368.9         165.5
2021-05-21 08:00:00+00:00           7324.8  44188.5         165.8
2021-05-21 16:00:00+00:00           7379.5  44977.8         164.1

Between 08:00 and 16:00 on May 19, 2021, XBQ open interest on alfa falls from 9,555 to 7,096 million USD, 25.7%. But the index falls 20.1% over the same interval, and open interest in USD mixes the two things: measured in coins, it goes from 179.4 to 166.6 thousand XBQ, 7.1% less. That is the part of the positions that was closed, almost all of it forcibly. A “deleveraging” indicator built with open interest in USD would confuse a price decline with a closing of positions.

What changes in a perpetuals backtest

  • Funding at its times. It is paid at 00:00, 08:00 and 16:00 UTC on the notional at that instant's mark, with that instant's position. Other exchanges use other schedules and other caps, and Binance documents that it may move some contracts to 1- or 4-hour intervals at times of volatility.
  • Premium with impact prices. The premium that drives funding is not that of the last price: the entire order book has to shift. Estimating funding with the difference between last price and index gives a noisy signal.
  • Liquidation with the intraday mark. Neither with the close, which hides almost half of the liquidations, nor with the last price, which exaggerates them in the wicks. And with the loss of the entire margin, without recovery.
  • Multi-exchange index. The mark and the PnL of an exchange depend on the spot prices of others; a local dislocation is not offset by the mark, and a decline in the index liquidates even if the exchange's own order book has not moved.
  • Open interest in coins. In USD it mixes price and positions.
  • The exchange is a counterparty. Insurance fund, ADL and, in the extreme, bankruptcy (lesson 3.7.2).

Exercises

  1. Repeat the experiment of the one-day longs with shorts: how many does the mark liquidate and how many the last price at 5x, 10x, 20x and 50x? Why is the difference between the two prices smaller than for longs?
Solution
IN [15]python
filas = []
for L in (5, 10, 20, 50):
    pl = precio_liquidacion(entrada, L, lado="corto")
    filas.append({"apal": L,
                  "por_mark": (g["mark_maximo"].max() >= pl).sum(),
                  "por_ultimo": (g["maximo"].max() >= pl).sum()})
pd.DataFrame(filas).set_index("apal")
OUT [15]Shorts liquidated by mark and by last price
      por_mark  por_ultimo
apal
5            0           0
10          22          22
20         239         254
50        1191        1232

At 10x, 22 shorts are liquidated with both prices; at 20x, 239 with the mark and 254 with the last price, and at 50x, 1,191 and 1,232. More shorts than longs are liquidated at 10x and 20x because over these six years XBQ rises much more than it falls. The difference between mark and last price is proportionally smaller because the planted wicks are all to the downside (long liquidation cascades); to the upside, the last price only separates from the mark by the normal difference in each hour's range.

  1. A long of 1 XEQ on the alfa perpetual, opened in the first period of 2021 with 3x leverage and held all year. Compute the funding paid in USD as a percentage of the initial margin and compare it with the simple sum of the rates. Why do they not match?
Solution
IN [16]python
xeq = alfa[(alfa["activo"] == "XEQ")
           & (alfa["ts"].dt.year == 2021)].set_index("ts")
cantidad = 1.0                                  # 1 XEQ of notional
pagos = cantidad * xeq["precio_mark"] * xeq["tasa_funding"]
nominal_ini = xeq["precio_mark"].iloc[0]
margen = nominal_ini / 3
print(f"payments: {pagos.sum():,.1f} USD per XEQ; initial margin "
      f"{margen:,.1f} USD ({100 * pagos.sum() / margen:.0f}%)")
print(f"simple sum of rates: {100 * xeq['tasa_funding'].sum():.1f}%")
OUT [16]Funding of a 3x XEQ long in 2021
payments: 913.9 USD per XEQ; initial margin 240.9 USD (379%)
simple sum of rates: 38.3%

The long pays 913.9 USD of funding over the year, 379% of the initial margin of 240.9 USD. The sum of rates is 38.3%, which at 3x would be 115% of the margin. The difference is the notional: it is paid on each period's mark, and XEQ multiplied during 2021, so the payments in the second half of the year, with the price several times higher, weigh much more than those of January. At the same time, the position gained from the price rise; funding was the cost of holding it with leverage instead of buying spot.

  1. You have to hedge a spot portfolio with a short on the perpetual and you are torn between isolated and cross margin on the short leg. Explain what happens in each case if XBQ rises 50% in a week, and what risk ADL adds to the hedge in a sell-off.
Solution

With isolated margin, the short leg only has its own margin: at 3x, its liquidation is around +33% (the lesson's formula for shorts gives +32.7%), so a 50% rise closes it; the spot portfolio is left unhedged right after the rise and the short leg's loss is crystallized. With cross margin, the account balance backs the position and liquidation comes later, but only if that balance is in the same account and counts as collateral (spot does not always count, or counts with a haircut); and one bad position can drag down the others. In both cases the defense is to top up margin in time, for example by selling part of the spot to move cash to the short leg. ADL acts the other way around: in a sharp sell-off, with many bankrupt longs and the insurance fund depleted, the engine closes winning positions on the other side, and a hedging short is exactly one of them. The spot portfolio is left unhedged in the middle of the sell-off without anyone having decided it.

Footnotes

  1. Binance, “What Are Mark Price and Price Index in USDS-Margined Futures?”: index as a weighted average of the spot prices of several exchanges, outliers clipped to ±3% of the median (±1% for some symbols) and zero weight for sources not updated in five minutes (link in the resources). ↩

  2. Binance, “Introduction to Binance Futures Funding Rates”: premium with impact prices, one sample every 5 seconds, time-weighted average of the period with increasing weights (1, 2, 3… n) over the 5,760 samples of 8 hours, default interval of 8 hours (00:00, 08:00 and 16:00 UTC), amount = notional (mark × size) × rate, and caps of ±0.75 times the maintenance rate on the main symbols (±2% on the rest). The caps in the data are specific to each fictitious exchange. ↩

  3. Binance, “What Are Mark Price and Price Index in USDS-Margined Futures?”: mark = median(price 1, price 2, contract price); under extreme conditions it may use only price 2. The mark is used for liquidations and unrealized PnL; realized PnL, with executed prices. ↩

  4. Binance, “Binance Futures Liquidation Protocols”: collateral = initial margin + realized PnL + unrealized PnL < maintenance margin; cancellation of open orders; an IOC order to reduce the shortfall; liquidation fee for Binance unless the position goes bankrupt; bankrupt positions pass to the insurance fund and, if it is not enough, to auto-deleveraging (ADL). Each exchange's maintenance rate also depends on the size of the position, in tiers; the lesson uses a fixed 0.5%. ↩

Each answer is folded under its question

Self-assessment

  1. The average premium of an 8-hour period is +4 basis points and the exchange uses Binance's formula with an interest of 0.01% per period. What funding is paid?

    • A+4 basis points: funding is the premium
    • B+5 basis points: premium plus interest
    • C0: the premium is smaller than the clamp
    • D+1 basis point: the premium is inside the −4 to +6 bp band, and the clamp returns exactly the interest
    Show answer

    Answer D. +1 basis point: the premium is inside the −4 to +6 bp band, and the clamp returns exactly the interest

    F = P + clamp(I − P, ±0.05%). With P = 0.04% and I = 0.01%, I − P = −0.03%, which does not exceed the clamp; F = 0.04% − 0.03% = 0.01%. In the data, 77.6% of XBQ periods on alfa pay exactly that basis point.

  2. In the lesson's example, the perpetual's order book falls 4% for 5 seconds and comes back. Why does Binance's mark fall only 0.65%?

    • ABecause the mark uses the day's closing price
    • BBecause it is the median of the funding-adjusted index, the index plus the 30-second average of the basis, and the last price: two of the three barely move in 5 seconds
    • CBecause the exchange cancels the trades in the wick
    • DBecause the mark is computed every 8 hours
    Show answer

    Answer B. Because it is the median of the funding-adjusted index, the index plus the 30-second average of the basis, and the last price: two of the three barely move in 5 seconds

    The 30-second average dilutes 5 seconds of decline to a sixth, and the median of three prices ignores the extreme. If the order book decline lasts five minutes, the 30-second average picks it up entirely and the mark falls as much as the last price: only the index, built from the spot prices of other exchanges, protects against a sustained dislocation.

  3. A 10x isolated long with a maintenance rate of 0.5%. How far from entry is it liquidated, and where is its bankruptcy price?

    • AIt is liquidated at −10% and goes bankrupt at −20%
    • BIt is liquidated at −0.5% and goes bankrupt at −10%
    • CIt is liquidated at −9.55% and goes bankrupt at −10%: between the two lies the maintenance margin with which the engine closes the position
    • DBoth at −10%
    Show answer

    Answer C. It is liquidated at −9.55% and goes bankrupt at −10%: between the two lies the maintenance margin with which the engine closes the position

    Liquidation comes when the collateral (margin plus PnL at the mark) equals the maintenance margin: M = E·(1 − 1/L)/(1 − mmr), −9.55%. The bankruptcy price, where the collateral is zero, is at −1/L = −10%. If the liquidation order executes worse than the bankruptcy price, the insurance fund pays the gap.

  4. With one-day 10x longs, the mark liquidates 13 out of 2,192 and the last price would liquidate 15. What is special about the two days of difference?

    • AThey are two planted candles in which the last price prints a low 4.5% below the mark's: cascading liquidation wicks
    • BThey are days without data
    • CThey are weekends
    • DThey are days with negative funding
    Show answer

    Answer A. They are two planted candles in which the last price prints a low 4.5% below the mark's: cascading liquidation wicks

    2022-11-08 and 2024-08-05 have planted candles: the mark falls 7.7% and 8.6% within the hour and the last price, 11.9% and 12.7%. At 10x, only the wick of the last price crosses the liquidation price. That is why exchanges liquidate with the mark.

  5. A backtest checks the liquidations of a 10x long using only each day's close. What does it find?

    • AFewer: 9 of the 13 that the mark liquidates hour by hour; in the other four the price recovers before the close
    • BThe same as with hourly data
    • CMore, because the close is more volatile
    • DNone, because the crypto market does not close
    Show answer

    Answer A. Fewer: 9 of the 13 that the mark liquidates hour by hour; in the other four the price recovers before the close

    A liquidation happens at the mark's intraday low, not at the close. A day that falls 12% and closes 6% down liquidates the 10x and does not look like it at the close; at 20x the difference is 195 versus 103 days.

Key points

  • The funding in the data follows Binance's formula: F = P + clamp(0.01% − P, ±0.05%), with P the average premium of the previous 8 hours and each exchange's cap. It is rebuilt without error on the four exchanges.
  • While the premium is between −4 and +6 basis points, funding is exactly 0.01% (77.6% of XBQ periods on alfa): almost 11% a year paid by the long in calm markets; in 2020 and 2021 it paid 25.6% and 24.4%.
  • The index is a median (or a capped average) of the spot prices of several exchanges: a 5.45% dislocation on cenit does not move it; a naive average of the four spot prices would have moved 1.32%.
  • The mark decides liquidation and unrealized PnL. A mark built from averages and medians ignores a wick of seconds, but not a sustained dislocation of the exchange's own order book: only the index absorbs that.
  • With isolated margin, liquidation comes at E·(1 − 1/L)/(1 − mmr) and bankruptcy at E·(1 − 1/L); the gap between execution and bankruptcy is paid by the insurance fund and, if it is not enough, by ADL.
  • Liquidations are measured with the intraday mark: daily closes hide almost half. From outside, aggregated liquidations reveal the engine's rule: nothing below a 0.6% decline and a fraction that grows with an exponent of ≈ 1.5.

Resources