Lesson 4.2.6 · 65 min

Derivatives risk: portfolio Greeks, margin and liquidations

A derivatives book is not managed with the price of each contract, but with its summed sensitivities, with the margin it requires and with the distance to liquidation. This lesson aggregates the Greeks of a book of index and stock options in dollars, puts limits on it, takes it through the April 2025 sell-off in the data and compares its margins; then it measures how often a pair of perpetuals is liquidated with isolated margin and with cross margin.

Fig. — The options book on 2 April 2025: P&L by underlying move according to its greeks (delta and gamma) and repricing every option. Highlighted, what it actually lost over the next two sessions
Level 4 lessons
  1. 4.1 · Portfolio theory and optimisation

    1. 4.1.1 Markowitz: efficient frontier and real constraints
    2. 4.1.2 Covariance estimation: shrinkage, factor models and denoising
    3. 4.1.3 Black-Litterman: from market equilibrium to views
    4. 4.1.4 Risk parity, minimum variance, HRP and HERC
    5. 4.1.5 Optimization with CVaR, drawdown, turnover and costs
    6. 4.1.6 Factor models: CAPM, Fama-French, PCA and attribution
    7. 4.1.7 Rebalancing and strategy portfolios
    8. 4.1.8 Portfolio tools: Riskfolio-Lib, PyPortfolioOpt, skfolio and bt
  2. 4.2 · Risk management and position sizing

    1. 4.2.1 Risk per trade, stops and risk of ruin
    2. 4.2.2 Kelly criterion and volatility targeting: how much to risk
    3. 4.2.3 VaR, Expected Shortfall and VaR backtesting
    4. 4.2.4 Stress testing and correlation, liquidity, gap and counterparty risk
    5. 4.2.5 Drawdown control and kill switch
    6. 4.2.6 Derivatives risk: portfolio Greeks, margin and liquidations
  3. 4.3 · Microstructure and execution

    1. 4.3.1 Implementation shortfall, VWAP, TWAP and Almgren-Chriss
    2. 4.3.2 Order flow imbalance, microstructure signals and latency
  4. 4.4 · Brokers and APIs

    1. 4.4.1 Choosing a broker from Spain or the EU: regulation, assets, costs and API
    2. 4.4.2 Interactive Brokers: TWS and IB Gateway, ib_async, contracts, data and orders
    3. 4.4.3 Alpaca, Tradier, MetaTrader 5 with Python and OANDA v20
    4. 4.4.4 Crypto exchange APIs: ccxt, Binance, Bybit, Kraken and Coinbase
    5. 4.4.5 Secure credentials: environment variables, secret managers and least privilege
  5. 4.5 · Live trading system

    1. 4.5.1 Live system architecture: from data to reconciliation
    2. 4.5.2 Order management: states, partial fills, cancellations and reconnections
    3. 4.5.3 Paper trading, shadow trading and slippage monitoring
    4. 4.5.4 Task scheduling and alerts: cron, APScheduler, Prefect, Telegram, Slack and email
    5. 4.5.5 Fault tolerance: idempotency, persistent state and reconciliation at startup
    6. 4.5.6 NautilusTrader or LEAN live: from backtest to production with the same code

From the contract to the book: Greeks in money

Lesson 3.4.3 calculated the Greeks of one option and lesson 3.5.4 built a tool that adds them up for a multi-leg position on a single underlying. A real book has options on several underlyings and several expiries, and whoever monitors it does not ask about the delta of a put, but things like “how much do I lose if the market falls 1%” or “how much do I gain if volatility rises one point”. For that the Greeks are converted into money, contract by contract, and added up:

Book GreekFormula per leg (nn contracts with multiplier MM)Reading
Delta per 1%nMΔS⋅1%n M \Delta S \cdot 1\%USD the book gains if the underlying rises 1%
Gamma per 1%12 nMΓ (1%⋅S)2\tfrac{1}{2}\, n M \Gamma\, (1\% \cdot S)^2USD that curvature adds in a 1% move (in a kk% move, k2k^2 times)
Vega per pointnMνn M \nuUSD per point of implied volatility
Theta per daynMΘn M \ThetaUSD the book gains or loses per calendar day with everything else equal

With them, the P&L of a kk% move is approximated by Taylor as P&L≈Δ1% k+Γ1% k2+ν Δσ+Θ Δt\text{P\&L} \approx \Delta_{1\%}\, k + \Gamma_{1\%}\, k^2 + \nu\, \Delta\sigma + \Theta\, \Delta t. The lesson tests that approximation with the synthetic data of level 3: the daily option chain of ORX, the fictitious index that imitates the S&P 500 (multiplier 100), and that of KRN, a fictitious stock with American options. The pricing engine is the Black-Scholes-Merton of lesson 3.5.4, unchanged, with each option's iv_modelo volatility, the rate interpolated on the curve and the index dividend yield:

IN [01]python
import numpy as np
import pandas as pd
from scipy.special import ndtr

AÑO, MULT = 365.0, 100   # T in calendar days / 365; 100 USD per point


def bsm(tipo, S, K, T, vol, r, q):
    """Black-Scholes-Merton from lesson 3.5.4: price and Greeks per
    unit; vega per volatility point, theta per calendar day."""
    S, K, T, vol = np.broadcast_arrays(
        *(np.asarray(x, dtype=float) for x in (S, K, T, vol)))
    vivo = T > 0
    Tv = np.where(vivo, T, 1.0)
    raiz = vol * np.sqrt(Tv)
    d1 = (np.log(S / K) + (r - q + vol**2 / 2) * Tv) / raiz
    d2 = d1 - raiz
    dq, dr = np.exp(-q * Tv), np.exp(-r * Tv)
    n1 = np.exp(-d1**2 / 2) / np.sqrt(2 * np.pi)
    w = 1.0 if tipo == "C" else -1.0
    precio = w * (S * dq * ndtr(w * d1) - K * dr * ndtr(w * d2))
    theta = (-S * dq * n1 * vol / (2 * np.sqrt(Tv))
             + w * q * S * dq * ndtr(w * d1)
             - w * r * K * dr * ndtr(w * d2))
    intr = np.maximum(w * (S - K), 0.0)
    return {"precio": np.where(vivo, precio, intr),
            "delta": np.where(vivo, w * dq * ndtr(w * d1),
                              w * (intr > 0)),
            "gamma": np.where(vivo, dq * n1 / (S * raiz), 0.0),
            "vega": np.where(vivo, S * dq * n1 * np.sqrt(Tv) / 100,
                             0.0),
            "theta": np.where(vivo, theta / AÑO, 0.0)}


idx = pd.read_csv("opciones3_indice.csv", parse_dates=["fecha"])
idx = idx.set_index("fecha")
krn = pd.read_csv("accion3_diario.csv", parse_dates=["fecha"])
krn = krn.set_index("fecha")
cadenas = {"ORX": pd.read_parquet("opciones3_cadena.parquet"),
           "KRN": pd.read_parquet("accion3_opciones.parquet")}
cadenas = {k: v[v["fecha"] >= "2025-03-01"].set_index(
    ["fecha", "vencimiento", "tipo", "strike"]).sort_index()
    for k, v in cadenas.items()}


def subyacente(sub, f):
    """Price and continuous dividend yield of the underlying.
    KRN has no ex-date before April 17: q = 0."""
    if sub == "ORX":
        return (idx.at[f, "cierre"],
                idx.at[f, "rentabilidad_dividendo"] / 100)
    return krn.at[f, "cierre"], 0.0


def tipo_libre(f, dias):
    """Continuous rate interpolated on the ORX curve (as a decimal)."""
    plazos = [30, 91, 182, 365, 730]
    t = idx.loc[f, ["tipo_1m", "tipo_3m", "tipo_6m", "tipo_1a",
                    "tipo_2a"]].to_numpy(dtype=float) / 100
    return float(np.interp(dias, plazos, t))

The book is set up on Monday, March 24, 2025 with a capital of 500,000 USD. It is a typical volatility-selling book: a short April strangle (8 5000 puts and 8 5500 calls), a short May straddle (2 calls and 2 puts at 5300), 8 May 4600 puts bought as tail insurance and 30 April 205 puts sold on KRN. The figures in the data are from a fictitious market:

IN [02]python
libro = pd.DataFrame(
    [("ORX", "2025-04-17", "P", 5000, -8),
     ("ORX", "2025-04-17", "C", 5500, -8),
     ("ORX", "2025-05-16", "P", 5300, -2),
     ("ORX", "2025-05-16", "C", 5300, -2),
     ("ORX", "2025-05-16", "P", 4600, 8),
     ("KRN", "2025-04-17", "P", 205, -30)],
    columns=["sub", "venc", "tipo", "strike", "n"])
libro["venc"] = pd.to_datetime(libro["venc"])
CAPITAL = 500_000


def griegas(f, mov=0.0, dvol=0.0, patas=None):
    """Value and USD Greeks of each leg on day f. mov: relative move
    of the underlyings; dvol: change in volatility as a decimal
    (0.10 = 10 points). patas: the book (by default)."""
    filas = []
    patas = libro if patas is None else patas
    for p in patas.itertuples():
        S, q = subyacente(p.sub, f)
        S = S * (1 + mov)
        dias = (p.venc - f).days
        clave = (f, p.venc, p.tipo, float(p.strike))
        iv = cadenas[p.sub].at[clave, "iv_modelo"] if dias > 0 else 0.2
        g = bsm(p.tipo, S, p.strike, dias / AÑO, iv + dvol,
                tipo_libre(f, max(dias, 1)), q)
        u = p.n * MULT
        filas.append({
            "pata": f"{p.sub} {p.venc:%d-%m} {p.tipo}{p.strike}",
            "valor": u * float(g["precio"]),
            "delta_1pct": u * float(g["delta"]) * S * 0.01,
            "gamma_1pct": u * 0.5 * float(g["gamma"]) * (0.01 * S)**2,
            "vega_punto": u * float(g["vega"]),
            "theta_dia": u * float(g["theta"])})
    return pd.DataFrame(filas).set_index("pata")


F0 = pd.Timestamp("2025-03-24")
g0 = griegas(F0)
g0.loc["TOTAL"] = g0.sum()
g0.round(0)
OUT [02]The book and its dollar Greeks
                   valor  delta_1pct  gamma_1pct  vega_punto  theta_dia
pata
ORX 17-04 P5000 -36227.0      8712.0      -844.0     -3087.0     1713.0
ORX 17-04 C5500 -18951.0     -8011.0     -1304.0     -2934.0     1109.0
ORX 16-05 P5300 -31437.0      5114.0      -281.0     -1600.0      251.0
ORX 16-05 C5300 -31355.0     -5419.0      -281.0     -1600.0      337.0
ORX 16-05 P4600  23842.0     -4173.0       319.0      2798.0     -761.0
KRN 17-04 P205   -4590.0      1254.0      -138.0      -456.0      222.0
TOTAL           -98718.0     -2524.0     -2530.0     -6879.0     2871.0

The book is worth −98,718 USD: it is the net premium collected, what it would cost to buy it all back. Its Greeks say it is a short-volatility book: vega of −6,879 USD per point, theta of +2,871 USD a day and a gamma of −2,530 USD for every 1% move (about −253,000 in a 10% move, if gamma did not change). The total delta is small, −2,524 USD per 1%, because the legs offset each other: the short April put contributes +8,712 and the short call, −8,011. A small delta made of two large deltas of opposite sign is fragile: any move breaks the balance.

Adding them up like this has two traps. The KRN delta is in USD per 1% of KRN, not of ORX: to add it you have to convert it with KRN's beta to the index (a 1% move in ORX moves KRN, on average, by β\beta%). And the vega of a near expiry is not equivalent to that of a distant one: the implied volatility of short tenors moves more. A common convention weights the vega of each tenor by 30/d\sqrt{30/d}, with dd the days to expiry, to express it as 30-day equivalent vega:

IN [03]python
r_orx = idx["cierre"].pct_change()
r_krn = krn["cierre"].pct_change()
ventana = slice("2024-03-25", "2025-03-24")
beta = (np.cov(r_krn[ventana], r_orx[ventana])[0, 1]
        / r_orx[ventana].var())
print(f"Beta of KRN against ORX: {beta:.2f}")
d = g0.drop("TOTAL")["delta_1pct"]
es_krn = d.index.str.startswith("KRN")
print(f"Delta summed as is:     {d.sum():9.0f} USD per 1%")
print(f"Delta per 1% of ORX:    "
      f"{d[~es_krn].sum() + beta * d[es_krn].sum():9.0f}")

orx = libro["sub"].eq("ORX").to_numpy()
tabla = pd.DataFrame({
    "vega": g0["vega_punto"].iloc[:-1].to_numpy()[orx],
    "dias": (libro.loc[orx, "venc"] - F0).dt.days.to_numpy()})
tabla["vega_30d"] = tabla["vega"] * np.sqrt(30 / tabla["dias"])
tabla.groupby("dias")[["vega", "vega_30d"]].sum().round(0)
OUT [03]Beta-weighted delta and vega by expiry
Beta of KRN against ORX: 0.00
Delta summed as is:         -2524 USD per 1%
Delta per 1% of ORX:        -3773
        vega  vega_30d
dias
24   -6021.0   -6732.0
53    -402.0    -303.0

KRN's beta against ORX over the last year is 0.00: in the level 3 data, KRN was generated without a market factor (over the whole sample, 0.09). It is a limitation of the data that comes in handy here: with the measured beta, the KRN delta adds no market risk and the book's delta in ORX terms is −3,773 USD per 1%, that of the ORX legs. With a real stock with beta 1 the 1,254 of KRN would be added. Vega by expiry shows what the total hides: the book is short 6,021 USD per point of April volatility (6,732 in 30-day equivalent vega) and almost flat in May (−402), because the 4600 puts bought offset the short straddle. A rise in short-term volatility larger than in long-term volatility, the normal thing in a sell-off, hurts it more than the total of −6,879 suggests.

Greek limits and why they are not enough

A Greek limit sets how much sensitivity the book accepts. The lesson's, for 500,000 USD of capital: delta between −5,000 and +5,000 USD per 1% (1% of capital for every 1% move), gamma of −3,000 USD per 1% at most and vega of −8,000 USD per point at most. The book is held without adjustments until April 16, the day before the April expiry, and is reviewed at the close of each session:

IN [04]python
fechas = idx.loc["2025-03-24":"2025-04-16"].index
serie = pd.DataFrame({f: griegas(f).sum() for f in fechas}).T
serie["pyg"] = serie["valor"] - serie["valor"].iloc[0]
serie["ORX"] = idx.loc[fechas, "cierre"]
LIM = {"delta_1pct": 5_000, "gamma_1pct": -3_000,
       "vega_punto": -8_000}
serie["fuera"] = ((serie["delta_1pct"].abs() > LIM["delta_1pct"])
                  | (serie["gamma_1pct"] < LIM["gamma_1pct"])
                  | (serie["vega_punto"] < LIM["vega_punto"]))
cols = ["ORX", "pyg", "delta_1pct", "gamma_1pct", "vega_punto",
        "theta_dia", "fuera"]
serie[cols].round(0)
OUT [04]The book's Greeks, day by day
               ORX       pyg  delta_1pct  gamma_1pct  vega_punto  theta_dia  fuera
2025-03-24  5277.0       0.0     -2524.0     -2530.0     -6879.0     2871.0  False
2025-03-25  5279.0    -500.0     -3059.0     -2503.0     -6698.0     3013.0  False
2025-03-26  5334.0   -9951.0     -8233.0     -2690.0     -7092.0     3357.0   True
2025-03-27  5333.0  -11068.0     -7588.0     -2745.0     -7000.0     3509.0   True
2025-03-28  5243.0    1063.0       807.0     -2425.0     -5889.0     3279.0  False
2025-03-31  5266.0   11521.0     -1396.0     -2468.0     -5297.0     3505.0  False
2025-04-01  5208.0   15550.0      3185.0     -2182.0     -4400.0     3331.0  False
2025-04-02  5211.0   20555.0      3521.0     -2210.0     -4176.0     3287.0  False
2025-04-03  4957.0  -28259.0     18487.0     -1542.0     -1484.0     2426.0   True
2025-04-04  4659.0 -168869.0     24871.0      -467.0      1844.0      426.0   True
2025-04-07  4824.0  -76774.0     22948.0     -1157.0       341.0     2379.0   True
2025-04-08  4821.0  -73556.0     22895.0     -1088.0       618.0     2363.0   True
2025-04-09  5280.0   -8359.0     -6330.0     -2227.0     -3310.0     9434.0   True
2025-04-10  5057.0    9814.0     10048.0     -1677.0     -1250.0     6304.0   True
2025-04-11  4942.0   -9342.0     17850.0     -1583.0       -51.0     4767.0   True
2025-04-14  4966.0   18609.0     18429.0     -2261.0       394.0     6953.0   True
2025-04-15  4953.0   21936.0     21754.0     -2842.0       802.0     7363.0   True
2025-04-16  5074.0   61733.0      6641.0     -2777.0       373.0    14606.0   True

The book starts within the three limits. It goes out on delta on March 26 and 27, with a 1% rise in ORX (−8,233 and −7,588 USD), and comes back in. On April 2 it is up 20,555 USD. On April 3 ORX falls 4.9% and on April 4 another 6.0%: the book loses 168,869 USD from the start, and its delta goes from +3,521 to +24,871 USD per 1%, five times the limit. The short April puts have gone into the money and behave like a long position in the index. From there the delta stays outside the limit every session until the 16th, also on April 9, when ORX rises 9.5% and the delta jumps to −6,330.

Fig. — The options book from 24 March to 16 April 2025: ORX and the book's delta in USD per 1% move. The fall pushes it out of its limit and keeps it out for ten sessions

Gamma never breaches its limit: in the sell-off it goes from −2,210 to −467 USD, because the deep in-the-money puts and the far out-of-the-money call have little curvature. Vega even changes sign (+1,844 on April 4), because of the weight of the 4600 puts bought. A Greek limit measures sensitivity at today's point; what changes in a large drop is the point. The figure at the top compares, for the book of April 2, what its Greeks predict with what revaluing each option gives:

IN [05]python
F1 = pd.Timestamp("2025-04-02")
base = griegas(F1)
v0 = base["valor"].sum()
d1, g1 = base["delta_1pct"].sum(), base["gamma_1pct"].sum()
filas = {}
for mov in (-0.15, -0.106, -0.08, -0.05, -0.02, 0.02, 0.05, 0.10):
    m = 100 * mov
    filas[f"{mov:+.1%}"] = {
        "delta": d1 * m,
        "delta_gamma": d1 * m + g1 * m**2,
        "completa": griegas(F1, mov)["valor"].sum() - v0,
        "completa_vol+10": griegas(F1, mov, 0.10 * (mov < 0))[
            "valor"].sum() - v0}
pd.DataFrame(filas).T.round(0)
OUT [05]Greeks against full revaluation
          delta  delta_gamma  completa  completa_vol+10
-15.0% -52816.0    -550059.0 -372678.0        -337789.0
-10.6% -37323.0    -285635.0 -226400.0        -209292.0
-8.0%  -28169.0    -169607.0 -144220.0        -143128.0
-5.0%  -17605.0     -72855.0  -66246.0         -83912.0
-2.0%   -7042.0     -15882.0  -15403.0         -50036.0
+2.0%    7042.0      -1798.0   -2337.0          -2337.0
+5.0%   17605.0     -37644.0  -45505.0         -45505.0
+10.0%  35211.0    -185786.0 -221008.0        -221008.0

For a 10.6% drop, the one of the next two days, delta alone predicts −37,323 USD; delta and gamma, −285,635; full revaluation, −226,400. Taylor exaggerates the loss in large drops because the gamma of the short puts falls as they go into the money (the derivative of gamma, speed, is not in the approximation), and understates it in large rises: with +10%, −185,786 against −221,008, because there the short calls gain gamma as they approach their strikes. With −15% the difference is 177,000 USD.

Volatility complicates the calculation further. With 10 more points in the drops, the loss of a −2% move goes from −15,403 to −50,036 USD (the book is short vega at the short end), but that of a −10.6% move falls from −226,400 to −209,292: at that point the 4600 puts bought, with a lot of vega, gain more from the rise in volatility than the short ones lose. The sign of the volatility effect depends on the size of the drop. What really happened:

IN [06]python
F2 = pd.Timestamp("2025-04-04")
real = serie.at[F2, "valor"] - serie.at[F1, "valor"]
print(f"ORX: {idx.at[F2, 'cierre'] / idx.at[F1, 'cierre'] - 1:+.2%}"
      f"   KRN: {krn.at[F2, 'cierre'] / krn.at[F1, 'cierre'] - 1:+.2%}")
print(f"Actual P&L of the book: {real:,.0f} USD"
      f" ({real / CAPITAL:+.1%} of capital)")
iv = cadenas["ORX"].xs(("2025-05-16", "P", 5300.0),
                       level=[1, 2, 3])["iv_modelo"]
print(f"IV of the May 5300 put: {iv[F1]:.1%} to {iv[F2]:.1%}")
por_pata = griegas(F2)["valor"] - base["valor"]
por_pata.round(0)
OUT [06]What happened on April 3 and 4
ORX: -10.60%   KRN: -2.25%
Actual P&L of the book: -189,424 USD (-37.9% of capital)
IV of the May 5300 put: 20.8% to 28.0%
pata
ORX 17-04 P5000   -247251.0
ORX 17-04 C5500      5494.0
ORX 16-05 P5300    -90906.0
ORX 16-05 C5300     19687.0
ORX 16-05 P4600    124845.0
KRN 17-04 P205      -1293.0
Name: valor, dtype: float64

The book lost 189,424 USD in two sessions, 37.9% of its capital, with ORX −10.6%, KRN −2.25% and the implied volatility of the May 5300 put going from 20.8% to 28.0%. Almost all of the loss comes from the short April put (−247,251) and the May one (−90,906); the tail insurance gained 124,845. The actual loss is smaller than the two revaluations (−226,400 with constant volatility and −209,292 with 10 more points) because volatility does not shift in parallel (it rises more at low strikes, lesson 3.4.5, and the 4600 puts bought gain more than in the scenario) and because two days of theta go by.

Margin: the exchange rule and portfolio margin

The broker does not ask for collateral based on the Greeks but based on a rule. Two families:

  • Strategy-based (the one in the Cboe SPX options specifications, already used in lesson 3.6.1): each uncovered short option requires 100% of the premium plus 15% of the index value times the multiplier, minus the out-of-the-money amount, with a minimum of the premium plus 10% of the index (calls) or of the strike (puts). Long options are paid in full. Spreads and combinations have their own, more favorable rules, which the lesson does not model: the figure that comes out is an upper bound.
  • Portfolio-based (portfolio margining): the largest theoretical loss of the set of positions on a grid of moves of the underlying. FINRA Rule 4210(g) sets, for a broad-based large-cap index, moves from −8% to +6% in ten equal intervals and a minimum of 0.375 USD per contract and unit of the multiplier (37.50 per ORX contract). In reality the theoretical prices come from a clearing-house model; the lesson uses the earlier Black-Scholes-Merton with each day's volatility.
IN [07]python
def margen_cboe(f):
    """SPX specifications: uncovered short = 100% of premium + 15%
    of the index - out-of-the-money amount; minimum: premium + 10% of
    the index (calls) or of the strike (puts). Longs are paid in full."""
    S, _ = subyacente("ORX", f)
    total = 0.0
    for p in libro[libro["sub"] == "ORX"].itertuples():
        clave = (f, p.venc, p.tipo, float(p.strike))
        if p.n > 0 or clave not in cadenas["ORX"].index:
            continue
        o = cadenas["ORX"].loc[clave]
        prima = (o["bid"] + o["ask"]) / 2
        fuera = max(S - p.strike, 0) if p.tipo == "P" else max(
            p.strike - S, 0)
        minimo = prima + 0.10 * (p.strike if p.tipo == "P" else S)
        total += -p.n * MULT * max(prima + 0.15 * S - fuera, minimo)
    return total


def margen_cartera(f, rejilla=np.linspace(-0.08, 0.06, 11)):
    """Worst loss of the ORX part at 11 points from -8% to +6%
    (FINRA 4210(g) for broad-based indices), with today's IV."""
    orx = libro["sub"].eq("ORX").to_numpy()
    v0 = griegas(f)["valor"].to_numpy()[orx].sum()
    perdidas = [griegas(f, m)["valor"].to_numpy()[orx].sum() - v0
                for m in rejilla]
    minimo = 0.375 * MULT * libro.loc[orx, "n"].abs().sum()
    return max(-min(perdidas), minimo)


filas = {}
for f in ["2025-03-24", "2025-04-02", "2025-04-04", "2025-04-08",
          "2025-04-16"]:
    f = pd.Timestamp(f)
    filas[f.date()] = {"capital": CAPITAL + serie.at[f, "pyg"],
                       "cboe_sin_cubrir": margen_cboe(f),
                       "cartera": margen_cartera(f)}
m = pd.DataFrame(filas).T
m["uso_cartera"] = m["cartera"] / m["capital"]
m.round(2)
OUT [07]Cboe margin and portfolio margin
              capital  cboe_sin_cubrir    cartera  uso_cartera
2025-03-24  500000.00        1296390.0  114516.63         0.23
2025-04-02  520555.12        1272542.8  126028.31         0.24
2025-04-04  331131.25        1581945.0  184350.02         0.56
2025-04-08  426444.50        1486100.0  202485.31         0.47
2025-04-16  561732.91        1307566.2  205251.86         0.37

The two rules give numbers of different orders of magnitude. Strategy-based, the index part of the book requires 1.30 million USD, more than double the capital: with that rule the book could not be opened without the offsets for spreads. With portfolio margin it requires 114,517 USD, 23% of capital. On April 4 the portfolio margin rises to 184,350 (56% of a capital that has fallen to 331,131) and keeps rising afterwards, even though the book recovers: 205,252 on April 16. It is the procyclicality that lesson 3.1.3 saw in futures.

The previous day's portfolio margin, 126,028 USD, covered an 8% drop with constant volatility. The drop was 10.6% with volatility rising, and the two-session loss exceeded it by 50%. Margin is a collateral rule calibrated for a few normal days, not a risk measure; a low margin allows more leverage, which is exactly what has to be watched.

Crypto liquidations: isolated and cross margin

In perpetuals (lesson 3.7.1) there is no margin call with days to top up: when collateral falls below the maintenance margin, the exchange's engine liquidates the position. Lesson 3.7.1 derived the liquidation price of an isolated position and warned that with cross margin the whole balance backs all positions. Here we measure what that means for a hedged position.

The data are the 1-hour bars of the XBQ and XEQ perpetuals on alfa (the exchange that imitates Binance), with each hour's mark, from 2020 to 2025. Every Monday at 00:00 UTC a position is opened and followed for 28 days with three variants of the same leverage LL (gross notional over collateral) and a maintenance margin of 0.5%:

  • Isolated XBQ long: liquidated if the hourly low of the mark touches E (1−1/L)/(1−m)E\,(1 - 1/L)/(1 - m).
  • Isolated pair: long XBQ and short XEQ with the same notional, each leg with half the collateral and the same leverage LL. It counts as liquidated if either leg is; the other one is left unhedged.
  • Cross pair: the same pair with a common collateral. It is liquidated when the account value (collateral plus the P&L of both legs at each hour's closing mark) falls below the maintenance of both.
IN [08]python
h = pd.read_parquet("cripto3_1h.parquet")
mark = h.pivot(index="ts", columns="activo", values="mark_cierre")
mini = h.pivot(index="ts", columns="activo", values="mark_minimo")
maxi = h.pivot(index="ts", columns="activo", values="mark_maximo")
MMR, H = 0.005, 24 * 28               # maintenance; 28 days
lunes = mark.index[(mark.index.dayofweek == 0)
                   & (mark.index.hour == 0)]
lunes = lunes[lunes <= mark.index[-1] - pd.Timedelta(hours=H)]
pos = mark.index.get_indexer(lunes)
X, LO, HI = mark.to_numpy(), mini.to_numpy(), maxi.to_numpy()


def liquidaciones(L):
    n = {"largo_XBQ": 0, "par_aislado": 0, "par_cruzado": 0}
    for i in pos:
        s = slice(i + 1, i + 1 + H)
        e1, e2 = X[i]                        # XBQ, XEQ
        liq_l = e1 * (1 - 1 / L) / (1 - MMR)   # lesson 3.7.1
        liq_c = e2 * (1 + 1 / L) / (1 + MMR)
        largo = (LO[s, 0] <= liq_l).any()
        n["largo_XBQ"] += largo
        n["par_aislado"] += largo or (HI[s, 1] >= liq_c).any()
        q1, q2 = L / 2 / e1, L / 2 / e2      # collateral 1, cross
        valor = 1 + q1 * (X[s, 0] - e1) - q2 * (X[s, 1] - e2)
        mant = MMR * (q1 * X[s, 0] + q2 * X[s, 1])
        n["par_cruzado"] += (valor <= mant).any()
    return {k: v / len(pos) for k, v in n.items()}


print(f"{len(pos)} entries, hourly XBQ-XEQ correlation: "
      f"{np.log(mark).diff().corr().iloc[0, 1]:.2f}")
liq = pd.DataFrame({L: liquidaciones(L) for L in (2, 3, 5, 10, 20)}).T
liq.rename_axis("apalancamiento").round(3)
OUT [08]Liquidations: long, isolated pair and cross pair
309 entries, hourly XBQ-XEQ correlation: 0.77
                largo_XBQ  par_aislado  par_cruzado
apalancamiento
2                   0.010        0.087        0.000
3                   0.039        0.256        0.003
5                   0.175        0.595        0.032
10                  0.359        0.909        0.184
20                  0.602        1.000        0.485

XBQ and XEQ have an hourly correlation of 0.77, so the pair hedges most of the risk of each leg. With cross margin it shows: at 5x 3.2% of the entries are liquidated against 17.5% for the long alone. With isolated margin, the same pair is worse than the long alone: 59.5% at 5x and 90.9% at 10x. Each leg has leverage LL and is liquidated by its own move, even if the other one gains the same. At 5x, the XEQ short is liquidated in 44% of the entries (77% of those in 2020 and 60% of those in 2021, years of strong rallies). A hedge with isolated margin is not a hedge.

Fig. — XBQ and XEQ perpetuals, 2020-2025: share of weekly entries liquidated within 28 days by leverage. The same long-short pair survives with cross margin and dies with isolated margin

Cross margin has its own risk: a bad position can consume the collateral of the others. At 20x, the cross pair is liquidated in almost half of the entries (48.5%): the spread between XBQ and XEQ moves enough to wipe out a 5% collateral. The distance to liquidation measured in standard deviations of the horizon works as an operational limit. For an isolated long, with the hourly volatility of the previous 30 days and the normal approximation without drift (reflection principle), the probability of touching the liquidation price within HH hours is 2 Φ(−d/(σH))2\,\Phi(-d / (\sigma \sqrt{H})), with dd the log distance:

IN [09]python
from scipy.stats import norm

lr = np.log(mark["XBQ"]).diff()
sig = lr.rolling(24 * 30).std().to_numpy()    # 30 days, hourly
filas = {}
for L in (2, 3, 5, 10, 20):
    prev, real = [], []
    for i in pos[5:]:
        e1 = X[i, 0]
        d = np.log((1 - MMR) / (1 - 1 / L))    # ln(entry/liq.)
        z = d / (sig[i] * np.sqrt(H))
        prev.append(2 * norm.cdf(-z))          # reflection, normal
        real.append((LO[i + 1:i + 1 + H, 0]
                     <= e1 * (1 - 1 / L) / (1 - MMR)).any())
    filas[L] = {"distancia_pct": 100 * (1 - (1 - 1 / L) / (1 - MMR)),
                "sigmas_28d_mediana": np.median(
                    d / (sig[pos[5:]] * np.sqrt(H))),
                "prevista": np.mean(prev), "real": np.mean(real)}
pd.DataFrame(filas).T.rename_axis("apalancamiento").round(3)
OUT [09]Distance to liquidation in sigmas
                distancia_pct  sigmas_28d_mediana  prevista   real
apalancamiento
2                      49.749               4.596     0.000  0.010
3                      32.998               2.674     0.018  0.039
5                      19.598               1.457     0.153  0.178
10                      9.548               0.670     0.489  0.365
20                      4.523               0.309     0.746  0.612

At 2x liquidation is 49.7% away from the price, 4.6 standard deviations of 28 days: the normal predicts no liquidation and in the data there are 1.0%. At 3x, 2.7 deviations, it predicts 1.8% and there are 3.9%. Where liquidation is far away, the normal underestimates a lot, in proportion, because of fat tails and jumps (the wicks of lesson 3.7.1). Where it is close, at 10x and 20x, it overstates (48.9% against 36.5%), because the sample has an upward drift that the formula does not include. A reasonable limit is not a fixed maximum leverage, but a minimum distance to liquidation in standard deviations of current volatility, with room for the tail: when volatility rises, the same position is closer to liquidation and has to be reduced before the engine does it.

A Greek check before every order

Limits only help if they are checked before sending each order, with the book that would result from executing it. The lesson's check adds up the Greeks of the book with the new leg and adds a scenario limit: the loss with a 10% drop and 10 more volatility points cannot exceed 20% of capital (100,000 USD).

noyesnoyesProposed orderBook + orderGreeks within limit?RejectStress within limit?To broker
Mermaid source
flowchart TB
  O[Proposed order] --> L[Book + order]
  L --> G{Greeks within limit?}
  G -- no --> R[Reject]
  G -- yes --> E{Stress within limit?}
  E -- no --> R
  E -- yes --> B[To broker]
IN [10]python
def revisar(patas, f, lim=LIM, perdida_max=0.20 * CAPITAL):
    """Greek limits and a scenario of -10% with +10 volatility
    points. Returns the list of breached limits."""
    tot = griegas(f, patas=patas).sum()
    estres = (griegas(f, -0.10, 0.10, patas=patas)["valor"].sum()
              - tot["valor"])
    rotos = []
    if abs(tot["delta_1pct"]) > lim["delta_1pct"]:
        rotos.append(f"delta {tot['delta_1pct']:,.0f}")
    if tot["gamma_1pct"] < lim["gamma_1pct"]:
        rotos.append(f"gamma {tot['gamma_1pct']:,.0f}")
    if tot["vega_punto"] < lim["vega_punto"]:
        rotos.append(f"vega {tot['vega_punto']:,.0f}")
    if estres < -perdida_max:
        rotos.append(f"scenario {estres:,.0f}")
    return rotos


def cabe(nueva, f):
    """Pre-order check: the book with the new leg."""
    prueba = pd.concat([libro, pd.DataFrame([nueva])],
                       ignore_index=True)
    rotos = revisar(prueba, f)
    return ("accept", []) if not rotos else ("reject", rotos)


print("Book on 24-03:", revisar(libro, F0))
print("Book on 02-04:", revisar(libro, F1))
venta = {"sub": "ORX", "venc": pd.Timestamp("2025-04-17"),
         "tipo": "P", "strike": 5000, "n": -4}
cobertura = {"sub": "ORX", "venc": pd.Timestamp("2025-05-16"),
             "tipo": "P", "strike": 4600, "n": 8}
print("Sell 4 more puts:", cabe(venta, F1))
print("Buy 8 4600 puts:", cabe(cobertura, F1))
OUT [10]A Greek check before every order
Book on 24-03: ['scenario -169,983']
Book on 02-04: ['scenario -193,086']
Sell 4 more puts: ('reject', ['delta 8,450', 'scenario -316,525'])
Buy 8 4600 puts: ('accept', [])

The check says something uncomfortable: the lesson's book did not pass the scenario limit on the day it was set up (−169,983 USD against a cap of −100,000), even though it met the three Greek limits. On April 2, one day before the drop, the scenario gave −193,086. Selling 4 more April puts is rejected on delta and on the scenario (−316,525); buying 8 more 4600 puts is accepted, because hedges reduce the scenario. A check like this does not prevent losses, but it makes the size of the book be decided by a scenario set in advance and not by the premium collected.

Common mistakes

  • Adding deltas of different underlyings without beta, or vegas of different tenors as if they were the same volatility.
  • Greek limits without a scenario limit. Greeks are local; the risk of an options book is in the large moves.
  • Scenarios with fixed volatility. In a sell-off volatility rises, more at short tenors and low strikes; the sign of the effect depends on the composition of the book.
  • Confusing margin with risk. Portfolio margin covers a grid set by the rule, not the worst possible drop, and it rises just when capital falls.
  • Hedging with isolated margin. Each leg is liquidated on its own; the hedge disappears at the worst moment.
  • Fixed leverage in crypto. The same position is closer to liquidation when volatility rises.

Exercises

  1. On April 4, the book's delta is +24,871 USD per 1%. Calculate how many ORX futures with a multiplier of 50 USD would have to be sold to bring it to zero and what that hedge would have gained or lost by the close of April 9, when ORX rises 9.5%. Compare it with the book's P&L on those days.
Solution
IN [11]python
F3 = pd.Timestamp("2025-04-09")
S2, S3 = idx.at[F2, "cierre"], idx.at[F3, "cierre"]
delta_usd_punto = serie.at[F2, "delta_1pct"] / (0.01 * S2)
futuros = -round(delta_usd_punto / 50)
cobertura = futuros * 50 * (S3 - S2)
libro_pyg = serie.at[F3, "valor"] - serie.at[F2, "valor"]
print(f"Delta: {delta_usd_punto:,.0f} USD per ORX point;"
      f" futures: {futuros}")
print(f"Book P&L from April 4 to 9: {libro_pyg:+,.0f}")
print(f"Hedge P&L:                  {cobertura:+,.0f}")
print(f"Hedged book:                "
      f"{libro_pyg + cobertura:+,.0f}")
OUT [11]Hedging the delta at the bottom
Delta: 534 USD per ORX point; futures: -11
Book P&L from April 4 to 9: +160,510
Hedge P&L:                  -341,528
Hedged book:                -181,018

The delta is equivalent to 534 USD per ORX point: 11 futures sold. From April 4 to 9 the book recovers 160,510 USD and the hedge loses 341,528: the hedged book loses 181,018 more. The hedge meets the limit and in this case it is expensive: selling futures at the bottom and having the market rebound 9.5% in one day takes away more than the book recovers. It is not an argument against hedging, because the drop could have continued: it is the cost of a short-gamma book, which forces you to sell after falling and buy after rising (lesson 3.6.2). The cheap way to meet the limits is not to build a book that breaks them at the first large move.

  1. Repeat the liquidation table with a maintenance margin of 1% and 2.5% (exchanges raise the maintenance rate with position size, in tiers). How does the advantage of cross margin change?
Solution
IN [12]python
filas = {}
for mmr in (0.005, 0.01, 0.025):
    MMR = mmr
    for L in (5, 10):
        filas[(mmr, L)] = liquidaciones(L)
MMR = 0.005
pd.DataFrame(filas).T.rename_axis(["mantenimiento", "apal"]).round(3)
OUT [12]Liquidations with higher maintenance
                    largo_XBQ  par_aislado  par_cruzado
mantenimiento apal
0.005         5         0.175        0.595        0.032
              10        0.359        0.909        0.184
0.010         5         0.181        0.608        0.036
              10        0.379        0.932        0.214
0.025         5         0.191        0.634        0.052
              10        0.430        0.958        0.285

A higher maintenance margin brings liquidation closer to the entry price in all three variants: the cushion left is roughly 1/L−m1/L - m. At 10x, with 2.5% maintenance the cushion drops from 9.5% to 7.7% and liquidations rise in all three: the long from 35.9% to 43.0%, the isolated pair from 90.9% to 95.8% and the cross pair from 18.4% to 28.5%. Cross margin is still by far the variant that is liquidated least, but it is the one that worsens most in proportion: its cushion was the spread between the two legs, which moves little, and each point of maintenance eats a larger part of it.

  1. In the revaluation table of April 2, 10 more volatility points worsen the P&L of a 2% drop and improve that of a 10.6% drop. Explain why with the composition of the book.
Solution

With the index near 5,200, the options with the most vega are those near the money: the short May 5300 straddle and the short April 5000 put. A rise in volatility with hardly any move makes them lose: the book is short vega near the money. After a 10.6% drop the index is at about 4,660 and the options near the money are the May 4600 puts bought, with 8 contracts and one month to expiry: now the book's vega is positive (+1,844 USD per point on April 4 in the lesson's table). Vega is not a fixed property of the book: it depends on where the underlying is, and a scenario has to move price and volatility at the same time.

Each answer is folded under its question

Self-assessment

  1. A book has a delta of −2,500 USD per 1% and a gamma of −2,500 USD per 1% move. What does it lose, according to its Greeks, if the underlying falls 10%?

    • A25,000 USD: the delta times 10
    • B2,500 USD
    • CAbout 225,000 USD: −2,500 × (−10) − 2,500 × 10² = 25,000 − 250,000
    • DNothing: the delta is small
    Show answer

    Answer C. About 225,000 USD: −2,500 × (−10) − 2,500 × 10² = 25,000 − 250,000

    The gamma term grows with the square of the move: 100 times the value per 1% in a 10% move. And even so it is an approximation: in the lesson, full revaluation gives −226,400 USD against −285,600 from delta and gamma in a 10.6% drop.

  2. Why does the lesson's book not breach its gamma limit in the April sell-off and still lose 37.9% of its capital in two sessions?

    • ABecause the Greeks measure sensitivity at today's point: with the drop, the short puts go into the money, their gamma falls and their delta soars; the risk of a large move is only measured by a scenario
    • BBecause the gamma was miscalculated
    • CBecause the gamma limit was too strict
    • DBecause the KRN options lost a lot
    Show answer

    Answer A. Because the Greeks measure sensitivity at today's point: with the drop, the short puts go into the money, their gamma falls and their delta soars; the risk of a large move is only measured by a scenario

    The book's gamma goes from −2,210 to −467 USD on April 4 while the delta rises from +3,521 to +24,871. A scenario limit (−10% with +10 volatility points) would have rejected the book on the same day it was set up: −169,983 USD against a cap of −100,000.

  3. With the Cboe rule for uncovered short options, the index part of the book requires 1.30 million USD; with a scenario-based portfolio margin, 114,500. Which number measures its risk better?

    • AThe Cboe one, because it is higher
    • BThe portfolio one, because it is what the broker asks for
    • CBoth are equal in risk
    • DNeither: the Cboe one ignores hedges and the portfolio one covers an 8% drop with today's volatility; two days later, the book had lost 189,400 USD
    Show answer

    Answer D. Neither: the Cboe one ignores hedges and the portfolio one covers an 8% drop with today's volatility; two days later, the book had lost 189,400 USD

    Margin is a collateral rule, not a risk measure. The lesson's portfolio margin covers −8% to +6% with constant volatility; the drop was 10.6% with volatility rising, and the loss exceeded the previous day's margin by 50%.

  4. A pair long XBQ and short XEQ with the same notional and leverage 5. Why is it liquidated in 59.5% of the entries with isolated margin and in 3.2% with cross margin?

    • ABecause cross margin has no maintenance
    • BBecause with isolated margin each leg has its own collateral and is liquidated by its own move; with cross margin the whole balance backs the positions and the two legs offset each other (correlation 0.77)
    • CBecause XEQ is more volatile
    • DBecause of the liquidation fee
    Show answer

    Answer B. Because with isolated margin each leg has its own collateral and is liquidated by its own move; with cross margin the whole balance backs the positions and the two legs offset each other (correlation 0.77)

    In isolated mode, a leg can be liquidated even if the other one gains the same, and the other one is left unhedged. In cross mode, what matters is the value of the pair, which moves much less than each leg.

  5. For a 10x long in XBQ, the normal approximation (reflection principle) predicts 48.9% liquidations in 28 days and in the data it is 36.5%; at 2x it predicts 0.0% and it is 1.0%. What does it indicate?

    • AThat the formula is wrong
    • BThat the normal underestimates the tail: it misses little where the threshold is close and a lot, in proportion, where the threshold is several deviations away
    • CThat leverage does not matter
    • DThat the last price should be used instead of the mark
    Show answer

    Answer B. That the normal underestimates the tail: it misses little where the threshold is close and a lot, in proportion, where the threshold is several deviations away

    At 2x liquidation is 4.6 standard deviations of 28 days away: with a normal it never happens, with fat tails it happens once in a hundred. At 10x, the upward drift of the sample and the reversal of moves reduce liquidations relative to the formula without drift.

Key points

  • The Greeks of a book are summed in money: delta and gamma in USD per 1% of the underlying, vega in USD per point and theta in USD per day. The delta of different underlyings is weighted by beta and the vega of different tenors is not simply added.
  • A Greek limit measures sensitivity at today's point. In the April 2025 sell-off in the data, the book's gamma fell and the delta was multiplied by seven: the book lost 37.9% of its capital in two sessions without breaching its gamma limit.
  • The Taylor approximation fails in large moves in both directions: with −15% it overstates the loss (−550,000 against −373,000 USD) and with +10% it understates it. The risk limits of an options book are set with scenarios, revaluing each option.
  • Margin does not measure risk: the Cboe uncovered rule ignores hedges (1.30 million) and the portfolio margin covers −8% with today's volatility (114,500). The two-session loss exceeded the previous day's portfolio margin.
  • With isolated margin, a hedged pair is liquidated by the losing leg even if the other one gains: at 5x, 59.5% of the entries against 3.2% with cross margin.
  • The distance to liquidation in standard deviations is a good limit, but with fat tails the normal underestimates distant liquidations.

Resources

Data