---
title: "4.2.6 Derivatives risk: Greeks, margin and liquidations"
description: "Dollar Greeks of an options book and their limits, scenarios against Taylor, Cboe and portfolio margin, and isolated or cross crypto liquidation."
url: "https://pythonparatrading.com/en/python-trading-course/level-4/derivatives-risk-greeks-margin-and-liquidations/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-4/derivatives-risk-greeks-margin-and-liquidations.png"
---

# 4.2.6 · 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.

- Course: [Level 4: Portfolios, risk, execution and live trading](https://pythonparatrading.com/en/python-trading-course/level-4/)
- Module: 4.2 · Risk management and position sizing
- Duration: 65 min
- Prerequisites: [3.4.3 · Greeks: delta, gamma, vega, theta, rho, vanna, volga and charm](https://pythonparatrading.com/en/python-trading-course/level-3/first-and-second-order-option-greeks/), [3.5.4 · A custom tool: payoff, aggregate Greeks and scenarios](https://pythonparatrading.com/en/python-trading-course/level-3/option-payoff-greeks-scenario-tool/), [3.6.1 · Systematic premium selling: put-write, strangles and their tail risk](https://pythonparatrading.com/en/python-trading-course/level-3/systematic-premium-selling-tail-risk/), [3.7.1 · Perpetuals: funding mechanics, mark price and liquidations](https://pythonparatrading.com/en/python-trading-course/level-3/perpetual-futures-funding-mark-price-liquidations/), [3.1.3 · Roll management, margins, SPAN and implied leverage](https://pythonparatrading.com/en/python-trading-course/level-3/futures-roll-margins-span-leverage/), [4.2.4 · Stress testing and correlation, liquidity, gap and counterparty risk](https://pythonparatrading.com/en/python-trading-course/level-4/stress-testing-correlation-liquidity-counterparty-risk/), [4.2.5 · Drawdown control and kill switch](https://pythonparatrading.com/en/python-trading-course/level-4/drawdown-control-and-kill-switch/)

## By the end of this lesson

- Aggregate the Greeks of a book with several underlyings and expiries in money units: delta and gamma per 1% move, vega per point and theta per day; weight delta by beta and vega by tenor.
- Set Greek and scenario limits, track them day by day and explain why a book within its Greek limits can lose a third of its capital in two sessions.
- Compare the Taylor approximation with full revaluation in large moves, with and without a change in volatility.
- Calculate the margin of a book with the Cboe rule for uncovered short options and with a scenario-based portfolio margin, and relate it to the possible loss.
- Measure the liquidation frequency of perpetuals by leverage with isolated and cross margin, and the distance to liquidation in standard deviations.

## 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 Greek | Formula per leg ($n$ contracts with multiplier $M$) | Reading |
|---|---|---|
| Delta per 1% | $n M \Delta S \cdot 1\%$ | USD the book gains if the underlying rises 1% |
| Gamma per 1% | $\tfrac{1}{2}\, n M \Gamma\, (1\% \cdot S)^2$ | USD that curvature adds in a 1% move (in a $k$% move, $k^2$ times) |
| Vega per point | $n M \nu$ | USD per point of implied volatility |
| Theta per day | $n M \Theta$ | USD the book gains or loses per calendar day with everything else equal |

With them, the P&L of a $k$% move is approximated by Taylor as
$\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:

```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:

```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)
```
```output
                   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 $\sqrt{30/d}$, with $d$ the days to expiry, to express it as 30-day
equivalent vega:

```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)
```
```output
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:

```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)
```
```output
               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.* Two panels by session. Top, ORX: from 5,277 on 24 March to 4,659 on 4 April and 5,074 on the 16th. Bottom, the book's delta with the ±5,000 USD limit bands: between −8,200 and +3,500 until 2 April (outside the limit on 26 and 27 March), it rises to +18,500 on the 3rd and +24,900 on 4 April (highlighted), and stays above the limit every following session except 9 April (−6,300, also outside).

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:

```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)
```
```output
          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:

```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)
```
```output
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.

<aside class="callout" data-kind="riesgo">

This book respected its Greek limits on the day it was set up and lost more than a third of its capital in two
sessions. It is not an extreme case in the data: it is what leveraged volatility selling does in a 10% drop (lesson
3.6.1). Size is decided with scenarios and with the worst known episode, not with the Greeks.

</aside>

## 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.

```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)
```
```output
              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 $L$ (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)$.
- **Isolated pair**: long XBQ and short XEQ with the same notional, each leg with half the collateral and the same
  leverage $L$. 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.

```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)
```
```output
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 $L$ 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.* Three curves against leverage (2, 3, 5, 10 and 20 times). Isolated pair (highlighted): 8.7%, 25.6%, 59.5%, 90.9% and 100%. Long XBQ: 1.0%, 3.9%, 17.5%, 35.9% and 60.2%. Cross-margined pair: 0%, 0.3%, 3.2%, 18.4% and 48.5%.

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 $H$ hours is
$2\,\Phi(-d / (\sigma \sqrt{H}))$, with $d$ the log distance:

```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)
```
```output
                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).

```mermaid
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]
```

```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))
```
```output
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.**


```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}")
```
```output
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.

2. 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.**


```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)
```
```output
                    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 - 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.

3. 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.

## 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%?

   - A. 25,000 USD: the delta times 10
   - B. 2,500 USD
   - C. About 225,000 USD: −2,500 × (−10) − 2,500 × 10² = 25,000 − 250,000
   - D. Nothing: the delta is small

   *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?

   - 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
   - B. Because the gamma was miscalculated
   - C. Because the gamma limit was too strict
   - D. Because the KRN options lost a lot

   *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?

   - A. The Cboe one, because it is higher
   - B. The portfolio one, because it is what the broker asks for
   - C. Both are equal in risk
   - 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

   *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?

   - A. Because cross margin has no maintenance
   - 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)
   - C. Because XEQ is more volatile
   - D. Because of the liquidation fee

   *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?

   - A. That the formula is wrong
   - 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
   - C. That leverage does not matter
   - D. That the last price should be used instead of the mark

   *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

### Research

- [Uses of Volatility and Current Volatility - VIX1D](https://pythonparatrading.com/en/uses-of-volatility/) · Research-0003 · 5 min

### Documentation

- [Cboe: SPX options specifications (margin of uncovered short options)](https://www.cboe.com/tradable_products/sp_500/spx_options/specifications/) · cboe.com
- [FINRA, Rule 4210: Margin Requirements (paragraph (g), portfolio margin)](https://www.finra.org/rules-guidance/rulebooks/finra-rules/4210) · finra.org

### Data

- [Level 3 data: daily ORX option chain (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/opciones3_cadena.parquet) · Download · opciones3_cadena.parquet
- [Level 3 data: KRN American options (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/accion3_opciones.parquet) · Download · accion3_opciones.parquet
- [Level 3 data: 1-hour bars of the alfa perpetuals (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/cripto3_1h.parquet) · Download · cripto3_1h.parquet

Previous lesson: [4.2.5 · Drawdown control and kill switch](https://pythonparatrading.com/en/python-trading-course/level-4/drawdown-control-and-kill-switch/)

Next lesson: [4.3.1 · Implementation shortfall, VWAP, TWAP and Almgren-Chriss](https://pythonparatrading.com/en/python-trading-course/level-4/implementation-shortfall-vwap-twap-almgren-chriss/)
