---
title: "3.5.3 Calendars, diagonals, ratios and risk reversals"
description: "Calendars, diagonals, double calendar, ratios, backspreads, risk reversal and jade lizard: two expirations, the skew and twelve years of history."
url: "https://pythonparatrading.com/en/python-trading-course/level-3/calendars-diagonals-ratios-risk-reversals/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-3/calendars-diagonals-ratios-risk-reversals.png"
---

# 3.5.3 · Calendars, diagonals, ratios, backspreads, risk reversals and jade lizard

> With two expirations, the P&L of a structure is no longer a kinked line: it depends on the volatility left in the long option. With different quantities in each leg, one side with unlimited risk appears. And with the skew, the same shape opens for a debit with calls and for a credit with puts. The lesson builds these structures with the chain of the ORX index and follows each one for 142 months.

- Course: [Level 3: Derivatives: futures, spreads, forex and options](https://pythonparatrading.com/en/python-trading-course/level-3/)
- Module: 3.5 · Option structures in Python
- Duration: 75 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.4.5 · Smile, skew and term structure; volatility surface with SVI](https://pythonparatrading.com/en/python-trading-course/level-3/volatility-smile-skew-svi-surface/), [3.5.1 · Directional and vertical structures: call, put, covered call, collar and spreads](https://pythonparatrading.com/en/python-trading-course/level-3/directional-and-vertical-option-spreads/), [3.5.2 · Volatility and range structures: straddles, strangles, condors and butterflies](https://pythonparatrading.com/en/python-trading-course/level-3/straddles-strangles-condors-butterflies/)

## By the end of this lesson

- Build calendars, diagonals and double calendars with two expirations of the chain and value them at the short expiration under different volatility assumptions.
- Compute the forward volatility between two expirations and explain what a calendar buys.
- Build ratios, backspreads, risk reversals and jade lizards, locate their side with unlimited risk and measure how the skew finances some of them.
- Aggregate the Greeks of structures with several expirations and different quantities, with their units.
- Measure the historical behavior of each structure and its worst episode, and check whether delta-based recipes produce the structure they claim.

## Two expirations, different quantities and the skew

Lessons 3.5.1 and 3.5.2 combined options of the same expiration with quantities of one at a time. This lesson breaks
both rules and uses a third thing that until now only appeared in passing: the skew.

- **Time structures.** A **calendar** sells a short-dated option and buys the one with the same strike and a longer
  expiration. A **diagonal** does the same with different strikes. A **double calendar** combines a put calendar below
  the index and a call calendar above it. They gain from the passage of time on the short leg, and their result depends
  on the volatility left in the long one.
- **Ratios and backspreads.** A **1×2 ratio** buys one option and sells two farther away; a **backspread** does the
  opposite. With more options on one side than on the other, one of the ends has no limit.
- **Skew structures.** A **risk reversal** sells an out-of-the-money put and buys an out-of-the-money call (or the
  other way around). A **jade lizard** sells a put and a call spread, with enough credit to have no upside risk.

The data is the same as in the previous lessons: the synthetic chain of the **ORX** index on December 31, 2025, now with
two expirations, January 16 (16 days) and February 20, 2026 (51 days); prices per unit (× 100 USD per contract), T in
calendar days / 365, continuous r and q for each expiration, theta per calendar day and vega per volatility point. The
first cell repeats the functions from 3.5.1. The data is synthetic, with a planted surface, skew and volatility premium:
it teaches the mechanics, not what a structure yields in the real market.

```python
import numpy as np
import pandas as pd
from scipy.stats import norm

cadena = pd.read_parquet("opciones3_cadena.parquet")
indice = pd.read_csv("opciones3_indice.csv", parse_dates=["fecha"],
                     index_col="fecha")
verdad = pd.read_parquet("opciones3_verdad.parquet")

HOY = pd.Timestamp("2025-12-31")
ENE = pd.Timestamp("2026-01-16")
VTO = FEB = pd.Timestamp("2026-02-20")
S0 = indice.loc[HOY, "cierre"]


def mercado(vto, fecha=HOY):
    """Continuous rate r and dividend q (decimal fraction) of an
    expiration: r from the curve; q implied by the forward."""
    v = verdad[(verdad.fecha == fecha)
               & (verdad.vencimiento == vto)].iloc[0]
    T = v.dias / 365                   # calendar days / 365
    S = indice.loc[fecha, "cierre"]
    return v.tipo_libre, v.tipo_libre - np.log(v.forward / S) / T


def bsm(S, K, T, sig, tipo, r, q):
    """Price (points), delta, gamma (per point), vega (per
    volatility point) and theta (per calendar day).
    S can be an array; with T = 0 it returns the payoff."""
    S = np.asarray(S, dtype=float)
    if T <= 0:
        pago = np.maximum(S - K, 0) if tipo == "C" else \
            np.maximum(K - S, 0)
        signo = 1.0 if tipo == "C" else -1.0
        delta = np.where(signo * (S - K) > 0, signo, 0.0)
        cero = np.zeros_like(S)
        return dict(precio=pago, delta=delta, gamma=cero,
                    vega=cero, theta=cero)
    raiz = sig * np.sqrt(T)
    d1 = (np.log(S / K) + (r - q + sig**2 / 2) * T) / raiz
    d2 = d1 - raiz
    dq, dr = np.exp(-q * T), np.exp(-r * T)
    gamma = dq * norm.pdf(d1) / (S * raiz)
    vega = S * dq * norm.pdf(d1) * np.sqrt(T)
    decay = -S * dq * norm.pdf(d1) * sig / (2 * np.sqrt(T))
    if tipo == "C":
        precio = S * dq * norm.cdf(d1) - K * dr * norm.cdf(d2)
        delta = dq * norm.cdf(d1)
        theta = (decay - r * K * dr * norm.cdf(d2)
                 + q * S * dq * norm.cdf(d1))
    else:
        precio = K * dr * norm.cdf(-d2) - S * dq * norm.cdf(-d1)
        delta = -dq * norm.cdf(-d1)
        theta = (decay + r * K * dr * norm.cdf(-d2)
                 - q * S * dq * norm.cdf(-d1))
    return dict(precio=precio, delta=delta, gamma=gamma,
                vega=vega / 100, theta=theta / 365)


def pata(tipo, K, n, vto=VTO, fecha=HOY):
    """A leg: n > 0 long, n < 0 short. Type "S" is the index
    itself (a portfolio that replicates it)."""
    if tipo == "S":
        return dict(tipo="S", K=0.0, n=n, bid=S0, ask=S0,
                    iv=0.0, vto=vto, r=0.0, q=0.0)
    o = cadena[(cadena.fecha == fecha)
               & (cadena.vencimiento == vto)
               & (cadena.tipo == tipo)
               & (cadena.strike == K)].iloc[0]
    rr, qq = mercado(vto, fecha)
    return dict(tipo=tipo, K=K, n=n, bid=o.bid, ask=o.ask,
                iv=o.iv_modelo, vto=vto, r=rr, q=qq)


def apertura(patas):
    """Cost in points at mid and crossing the bid-ask spread:
    long legs at the ask, short legs at the bid (> 0 = debit)."""
    medio = sum(p["n"] * (p["bid"] + p["ask"]) / 2 for p in patas)
    real = sum(p["n"] * (p["ask"] if p["n"] > 0 else p["bid"])
               for p in patas)
    return medio, real


def valor(patas, S, fecha, griega="precio"):
    """Value (or Greek) of the structure if the index is at S
    on `fecha`, with each leg's IV frozen."""
    S = np.asarray(S, dtype=float)
    total = np.zeros_like(S)
    for p in patas:
        if p["tipo"] == "S":
            x = {"precio": S, "delta": np.ones_like(S)}
            total += p["n"] * x.get(griega, np.zeros_like(S))
            continue
        T = (p["vto"] - fecha).days / 365
        g = bsm(S, p["K"], T, p["iv"], p["tipo"], p["r"], p["q"])
        total += p["n"] * g[griega]
    return total


REJILLA = np.arange(1.0, 3 * S0, 1.0)   # from 1 point to 3 times S0


def resumen(patas):
    """Cost, maximum, minimum and breakeven points at expiration
    (the structure's first one), in points per unit."""
    medio, real = apertura(patas)
    fin = min(p["vto"] for p in patas)
    pnl = valor(patas, REJILLA, fin) - real
    i = np.nonzero(np.diff(np.sign(pnl)))[0]
    muertos = REJILLA[i] - pnl[i] / (pnl[i + 1] - pnl[i])
    muertos = np.unique(np.round(muertos, 1))
    final = pnl[-1] - pnl[-2]          # slope toward large S
    tol = 0.01       # with two expirations, the dividend leaves a
    return {"medio": round(float(medio), 2),   # tiny slope
            "real": round(float(real), 2),
            "horquilla": round(float(real - medio), 2),
            "maximo": "unlimited" if final > tol
            else round(float(pnl.max()), 2),
            "minimo": "unlimited" if final < -tol
            else round(float(pnl.min()), 2),
            "muertos": [round(float(x), 1) for x in muertos]}


def griegas(patas, S=S0, fecha=HOY):
    """Aggregate Greeks per unit (× 100 USD per contract)."""
    g = {k: float(valor(patas, S, fecha, k))
         for k in ["delta", "gamma", "vega", "theta"]}
    return {"delta": round(g["delta"], 3),
            "gamma": round(g["gamma"], 5),
            "vega": round(g["vega"], 2),
            "theta": round(g["theta"], 2)}


def con_delta(fecha=HOY, vto=VTO):
    """The chain of one expiration with the BSM delta of each row."""
    d = cadena[(cadena.fecha == fecha)
               & (cadena.vencimiento == vto)].copy()
    rr, qq = mercado(vto, fecha)
    S, T = indice.loc[fecha, "cierre"], (vto - fecha).days / 365
    for t in ("C", "P"):
        m = d.tipo == t
        d.loc[m, "delta"] = bsm(S, d.strike[m].to_numpy(), T,
                                d.iv_modelo[m].to_numpy(), t,
                                rr, qq)["delta"]
    return d


def strike_delta(d, tipo, objetivo):
    x = d[d.tipo == tipo]
    return float(x.strike.iloc[(x.delta - objetivo).abs().argmin()])
```

## Three expirations, three volatilities

The daily chain lists the three nearest monthly expirations. What changes between them is what drives the time
structures:

```python
def iv_delta(d, tipo, objetivo):
    """IV of the strike with the delta closest to the target."""
    k = strike_delta(d, tipo, objetivo)
    return d.iv_modelo[(d.tipo == tipo) & (d.strike == k)].iloc[0]


filas = []
for vto in sorted(cadena.vencimiento[cadena.fecha == HOY].unique()):
    d = con_delta(HOY, vto)
    c = d[(d.tipo == "C") & (d.strike == 6100)].iloc[0]
    r_, q_ = mercado(vto)
    filas.append({"vencimiento": vto.date(),
                  "dias": (vto - HOY).days, "r": r_, "q": q_,
                  "call_6100": (c.bid + c.ask) / 2,
                  "iv_6100": c.iv_modelo,
                  "iv_c25": iv_delta(d, "C", 0.25),
                  "iv_p25": iv_delta(d, "P", -0.25)})
pd.DataFrame(filas).set_index("vencimiento").round(4)
```
```output
             dias       r       q  call_6100  iv_6100  iv_c25  iv_p25
vencimiento
2026-01-16     16  0.0369  0.0117       46.2   0.0946  0.0740  0.1195
2026-02-20     51  0.0368  0.0117      119.3   0.1258  0.1007  0.1539
2026-03-20     79  0.0366  0.0117      167.6   0.1390  0.1093  0.1728
```

The implied volatility of the 6,100 call rises with maturity: 0.0946 at 16 days, 0.1258 at 51 and 0.1390 at 79. It is
the normal term structure of a calm market, in contango (lesson 3.4.5); in the data it inverts in crises. The skew also
grows in points with maturity: the difference between the 25D call and put (delta of 0.25 in absolute value) is −4.55
points in January, −5.32 in February and −6.35 in March.

A calendar that sells January and buys February does not buy “February volatility”, but that of the stretch the
February option has ahead of it when the January one expires. Since total variances add up over time, that forward
volatility can be solved from the two implied volatilities:

$$
\sigma_{1,2}^2 = \frac{\sigma_2^2 T_2 - \sigma_1^2 T_1}{T_2 - T_1}.
$$

```python
t1, t2 = 16 / 365, 51 / 365
s1, s2 = 0.0946, 0.1258              # IV of the 6100 call
var_plazo = (s2**2 * t2 - s1**2 * t1) / (t2 - t1)
print(f"forward volatility January 16-February 20: "
      f"{np.sqrt(var_plazo):.4f}")
```
```output
forward volatility January 16-February 20: 0.1377
```

13.77%, above either of the two implied volatilities: the market prices in that the January-to-February stretch will
be more volatile than the next 16 days. Whether the forward volatility is expensive or cheap compared with what is
expected is the question a calendar asks. Lesson 3.6.2 covers term structure trades systematically.

## The call calendar

```python
calendario = [pata("C", 6100, -1, ENE), pata("C", 6100, 1, FEB)]
print(resumen(calendario))
print(griegas(calendario))
for p in calendario:
    print(p["vto"].date(), p["n"],
          {k: round(float(valor([p], S0, HOY, k)), 3)
           for k in ["vega", "theta", "gamma"]})
```
```output
{'medio': 73.1, 'real': 73.9, 'horquilla': 0.8, 'maximo': 28.15, 'minimo': -73.9, 'muertos': [6041.7, 6168.3]}
{'delta': 0.032, 'gamma': -0.00192, 'vega': 3.97, 'theta': 0.38}
2026-01-16 -1 {'vega': -5.082, 'theta': 1.704, 'gamma': -0.003}
2026-02-20 1 {'vega': 9.049, 'theta': -1.323, 'gamma': 0.001}
```

The 6,100 calendar costs 73.9 points crossing the bid-ask spread (73.1 at mid): the January call is sold at the bid and
the February one bought at the ask. Its Greeks are the sum of two very different options. The January one, at 16 days,
has more theta (+1.70 points a day sold) and more gamma; the February one has more vega (9.05 versus 5.08). The
balance: positive theta (+0.38), positive vega (+3.97), negative gamma (−0.00192) and delta almost zero (0.032). The
calendar collects the passage of time like a premium sale, but it is long volatility: if implied volatility rises, it
gains.

`resumen` values it on January 16, the first expiration: the January call settles at its intrinsic value and the
February one is valued with BSM at 35 days with today's volatility. The maximum gain, 28.15 points, is at the strike;
the maximum loss is the whole debit, when the index moves so far in either direction that both calls are worth the
same (both almost nothing, or both their intrinsic value). The breakeven points, 6,041.7 and 6,168.3, leave a range of
±1% around the strike.

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

With two expirations, the slope of the P&L far from the strike is not exactly zero: the long February call, deep in the
money, has a delta of $e^{-qT}$ (0.9989) and not 1. That is why this lesson's version of `resumen` tolerates slopes
of less than one hundredth before calling an end “unlimited”.

</aside>

```mermaid
flowchart TB
  H[Dec 31: open] --> C[Sell 6100 January]
  H --> L[Buy 6100 Feb.]
  C --> E[Jan 16: intrinsic]
  L --> V[Jan 16: BSM with IV]
  E --> P[Calendar P&L]
  V --> P
```

On January 16, the February leg no longer has a known volatility. The header figure draws the P&L on that day under
three assumptions:

```python
def con_iv(patas, dv):
    """The same legs with the IV shifted by dv (decimal fraction)."""
    return [{**p, "iv": p["iv"] + dv} for p in patas]


coste = apertura(calendario)[1]
S = np.array([5800.0, 5950, 6050, 6100, 6150, 6250, 6400])
tabla = pd.DataFrame({
    f"IV {dv * 100:+.0f}": valor(con_iv(calendario, dv), S, ENE)
    - coste for dv in (-0.03, 0.0, 0.03)}, index=S)
tabla.index.name = "ORX on January 16"
tabla.round(1)
```
```output
                   IV -3  IV +0  IV +3
ORX on January 16
5800.0             -70.0  -61.8  -49.8
5950.0             -50.9  -32.9  -13.5
6050.0             -18.5    3.7   25.9
6100.0               5.7   28.2   50.7
6150.0             -14.8    6.8   28.8
6250.0             -41.1  -24.3   -5.4
6400.0             -56.2  -48.4  -36.6
```

With the index at 6,100, the calendar gains 28.2 if February volatility stays the same, 50.7 if it rises 3 points and
only 5.7 if it drops 3. Three volatility points are worth about 22 points of P&L, almost the same as the maximum gain:
the vega of the February call at 35 days is about 7.5 points per point. A 3-point drop is not a rare scenario: it takes
February volatility (0.1258) to the level of January's (0.0946), something that happens every time the market calms
down and the term structure flattens. At 5,800 or at 6,400 the calendar loses between 36 and 70 points with any
volatility: far from the strike, vega no longer saves anything.

## Diagonal and double calendar

```python
diagonal = [pata("C", 6200, -1, ENE), pata("C", 6000, 1, FEB)]
doble = [pata("P", 5900, -1, ENE), pata("P", 5900, 1, FEB),
         pata("C", 6300, -1, ENE), pata("C", 6300, 1, FEB)]
for nombre, e in [("diagonal 6000 Feb / 6200 Jan", diagonal),
                  ("double calendar 5900/6300", doble)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
```
```output
diagonal 6000 Feb / 6200 Jan {'medio': 181.9, 'real': 182.7, 'horquilla': 0.8, 'maximo': 62.5, 'minimo': -182.7, 'muertos': [6116.6]}
    {'delta': 0.506, 'gamma': -0.00119, 'vega': 5.62, 'theta': -0.7}
double calendar 5900/6300 {'medio': 71.7, 'real': 72.65, 'horquilla': 0.95, 'maximo': 33.83, 'minimo': -93.41, 'muertos': [5831.8, 5992.6, 6239.6, 6348.8]}
    {'delta': 0.072, 'gamma': 0.00064, 'vega': 10.86, 'theta': -0.49}
```

- **6,000 February / 6,200 January diagonal.** It buys an in-the-money February call and sells an out-of-the-money
  January one. It costs 182.7 and has a delta of 0.506: it is a bullish position, a covered call with the February
  option instead of the index (lesson 3.5.1 explains the covered call). It gains at most 62.5 if the index ends at
  6,200 on January 16, and its theta is negative (−0.70): the 6,000 February call loses time value faster than the
  6,200 January one collects it, being far out of the money at 16 days. A diagonal is not a calendar with another
  strike: it is a different position.
- **5,900/6,300 double calendar.** Two calendars, a put one at 5,900 and a call one at 6,300. It costs 72.65 and has
  two humps, with four breakeven points (5,831.8, 5,992.6, 6,239.6 and 6,348.8): it gains if the index ends near one of
  the two strikes and loses if it stays in the center or moves far away. Its vega is the highest in the lesson (10.86)
  and today its gamma is positive and its theta negative: with the index between the two strikes, the short January
  options are far out of the money at 16 days and worth little; the long February ones dominate. It turns short gamma
  when the index approaches one of the strikes.

## Ratios and backspreads

```python
ratio_c = [pata("C", 6100, 1), pata("C", 6300, -2)]
ratio_p = [pata("P", 6100, 1), pata("P", 5900, -2)]
back_c = [pata("C", 6100, -1), pata("C", 6300, 2)]
back_p = [pata("P", 6100, -1), pata("P", 5900, 2)]
for nombre, e in [("call ratio 1x2 6100/6300", ratio_c),
                  ("put ratio 1x2 6100/5900", ratio_p),
                  ("call backspread 6100/6300", back_c),
                  ("put backspread 6100/5900", back_p)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
```
```output
call ratio 1x2 6100/6300 {'medio': 64.5, 'real': 65.6, 'horquilla': 1.1, 'maximo': 134.4, 'minimo': 'unlimited', 'muertos': [6165.6, 6434.4]}
    {'delta': 0.095, 'gamma': -0.00115, 'vega': -4.21, 'theta': 0.16}
put ratio 1x2 6100/5900 {'medio': -5.0, 'real': -3.7, 'horquilla': 1.3, 'maximo': 203.7, 'minimo': -5695.3, 'muertos': [5696.3]}
    {'delta': 0.047, 'gamma': -0.00047, 'vega': -5.74, 'theta': 1.09}
call backspread 6100/6300 {'medio': -64.5, 'real': -63.4, 'horquilla': 1.1, 'maximo': 'unlimited', 'minimo': -136.6, 'muertos': [6163.4, 6436.6]}
    {'delta': -0.095, 'gamma': 0.00115, 'vega': 4.21, 'theta': -0.16}
put backspread 6100/5900 {'medio': 5.0, 'real': 6.3, 'horquilla': 1.3, 'maximo': 5692.7, 'minimo': -206.3, 'muertos': [5693.7]}
    {'delta': -0.047, 'gamma': 0.00047, 'vega': 5.74, 'theta': -1.09}
```

*Fig. — February 2026 6,100/6,300 1×2 call ratio, 6,100/5,900 1×2 put ratio and 6,100/5,900 put backspread: P&L at expiry with the opening at the bid and ask.* Three expiry P&L lines against ORX between 5,500 and 6,700. The call ratio costs 65.6, gains up to 134.4 at 6,300 and loses without limit above 6,434.4. The put ratio opens for a 3.7-point credit, highlighted, earned across the whole upside; it gains up to 203.7 at 5,900 and loses without limit below 5,696.3. The put backspread is its mirror image: it loses up to 206.3 at 5,900 and gains on large falls.

The 1×2 call ratio buys the 6,100 and sells two 6,300. It costs 65.6 and gains at most 134.4 if the index ends at
6,300; above 6,434.4 it loses without limit, because one short call is left uncovered. The put ratio, with the same
middle strike and the 5,900 put, **collects** 3.7 points: it gains that if the index rises, gains up to 203.7 at 5,900
and loses without limit below 5,696.3 (6.5% below the index). The difference is the skew: the 25D put has a volatility
of 15.39% and the 25D call, 10.07%. Two 5,900 puts are worth almost the same as one 6,100; two 6,300 calls are worth
less than half of one 6,100.

```mermaid
flowchart TB
  S[Put skew] --> RP[Put ratio]
  S --> RR[Risk reversal]
  S --> JL[Jade lizard]
  RP --> C[Left tail]
  RR --> C
  JL --> C
```

Backspreads are the mirror image. The call backspread sells the 6,100 and buys two 6,300: it opens for a credit of
63.4 and gains without limit on strong rallies, but loses up to 136.6 if the index ends at 6,300. The put one sells the
6,100 and buys two 5,900: it costs 6.3, loses up to 206.3 at 5,900 and gains on sharp falls. It is left-tail
protection with a weak spot in the moderate-decline zone. Its Greeks say so: vega +5.74 and theta −1.09; it pays for
time and for the skew.

## Risk reversal and jade lizard

```python
risk_rev = [pata("P", 5900, -1), pata("C", 6300, 1)]
jade = [pata("P", 6000, -1), pata("C", 6200, -1), pata("C", 6300, 1)]
for nombre, e in [("risk reversal 5900/6300", risk_rev),
                  ("jade lizard 6000/6200/6300", jade)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
iv_c = risk_rev[1]["iv"]
iv_p = risk_rev[0]["iv"]
print(f"IV call 25D {iv_c:.4f}  IV put 25D {iv_p:.4f}"
      f"  risk reversal {100 * (iv_c - iv_p):.2f} points")
```
```output
risk reversal 5900/6300 {'medio': -29.5, 'real': -28.8, 'horquilla': 0.7, 'maximo': 'unlimited', 'minimo': -5870.2, 'muertos': [5871.2]}
    {'delta': 0.475, 'gamma': 0.00034, 'vega': -0.76, 'theta': 0.26}
jade lizard 6000/6200/6300 {'medio': -114.9, 'real': -113.7, 'horquilla': 1.2, 'maximo': 113.7, 'minimo': -5885.3, 'muertos': [5886.3]}
    {'delta': 0.195, 'gamma': -0.00137, 'vega': -10.42, 'theta': 1.36}
IV call 25D 0.1007  IV put 25D 0.1539  risk reversal -5.32 points
```

The 5,900/6,300 **risk reversal** sells the 25D put and buys the 25D call. It collects 28.8 points: the put costs more
than the call because its volatility is 5.32 points higher. That −5.32 is the market definition of the risk reversal as
a measure of skew (the volatility of the 25D call minus that of the 25D put), and the structure is the way to trade it:
delta 0.475, vega almost zero (−0.76), theta +0.26. It is a synthetic long in the index with half the delta, which
collects the skew in exchange for the left tail: below 5,871.2 it loses like a short put.

The 6,000/6,200/6,300 **jade lizard** sells the 6,000 put and the 6,200/6,300 call spread. It collects 113.7 points,
more than the width of the call spread (100): if the index rises above 6,300 it gains 13.7 instead of losing. It has no
upside risk. It has all the risk on the downside (breakeven point at 5,886.3) and it is the shortest-volatility
structure in the lesson: vega −10.42, theta +1.36.

## What the extra legs cost

```python
estructuras = {"calendar": calendario, "diagonal": diagonal,
               "double calendar": doble, "call ratio": ratio_c,
               "put ratio": ratio_p, "put backspread": back_p,
               "risk reversal": risk_rev, "jade lizard": jade}
filas = []
for nombre, e in estructuras.items():
    medio, real = apertura(e)
    filas.append({"estructura": nombre,
                  "contratos": sum(abs(p["n"]) for p in e),
                  "prima_medio": medio, "horquilla": real - medio,
                  "pct_prima": 100 * (real - medio) / abs(medio)})
pd.DataFrame(filas).set_index("estructura").round(2)
```
```output
                 contratos  prima_medio  horquilla  pct_prima
estructura
calendar                 2         73.1       0.80       1.09
diagonal                 2        181.9       0.80       0.44
double calendar          4         71.7       0.95       1.32
call ratio               3         64.5       1.10       1.71
put ratio                3         -5.0       1.30      26.00
put backspread           3          5.0       1.30      26.00
risk reversal            2        -29.5       0.70       2.37
jade lizard              3       -114.9       1.20       1.04
```

With two legs and a large net premium, the bid-ask spread is less than 1.1% of the premium (calendar, diagonal, jade
lizard). Structures with an almost zero net premium are another story: the put ratio and the put backspread, with a mid
premium of 5 points, lose 26% of it crossing the spread. A structure that “opens for free” at mid does not open for
free.

## The skew over twelve years

The risk reversal is also a time series: how much more expensive the put is than the call with the same delta, month
by month. The cell also prepares the entries for the history in the next section, which needs the chain of the short
expiration and that of the long one:

```python
vtos = np.sort(cadena.vencimiento.unique())
sesiones = indice.index
por_dia = dict(tuple(cadena.groupby(["fecha", "vencimiento"])))
entradas = []           # (session, short exp, long exp, chains)
for ant, v1, v2 in zip(vtos[:-2], vtos[1:-1], vtos[2:]):
    if v2 > sesiones[-1]:
        break
    f = sesiones[sesiones.searchsorted(ant, side="right")]
    if (f, v1) in por_dia and (f, v2) in por_dia:
        entradas.append((f, v1, v2, con_delta(f, v1),
                         con_delta(f, v2)))


rr = pd.DataFrame({
    "rr25": [100 * (iv_delta(d1, "C", 0.25) - iv_delta(d1, "P", -0.25))
             for f, v1, v2, d1, d2 in entradas],
    "iv_atm": [100 * iv_delta(d1, "C", 0.50)
               for f, v1, v2, d1, d2 in entradas]},
    index=[e[0] for e in entradas])
print(len(rr), "entries;", rr.rr25.describe().round(2).to_dict())
rr.loc[[rr.rr25.idxmin(), rr.rr25.idxmax()]].round(2)
```
```output
142 entries; {'count': 142.0, 'mean': -7.08, 'std': 2.79, 'min': -22.65, '25%': -8.19, '50%': -6.7, '75%': -5.1, 'max': -2.91}
             rr25  iv_atm
2020-03-23 -22.65   62.80
2014-01-21  -2.91    8.97
```

*Fig. — 25-delta risk reversal (25D call IV minus 25D put IV, in volatility points) of the next monthly expiry, in the 142 trades from 2014 to 2025.* Time series from 2014 to 2025, always negative: the 25D put trades at a higher volatility than the 25D call in every trade. The median is -6.7 points (dashed line); in crises the risk reversal plunges, and the minimum, highlighted, is -22.7 points at the 23 March 2020 trade.

In the 142 entries the 25D risk reversal of the next monthly expiration is always negative: the median, −6.7 points;
the least negative, −2.91 in January 2014, with ATM volatility at 8.97%; the most, −22.65 on March 23, 2020, with ATM
at 62.80%. The data README gives a median of −5.4 points in calm periods for 25- to 35-day slices; here all regimes
are included, and the skew in points grows with the level of volatility. A risk reversal sold “because the skew is
expensive” is sold when the market is already scared: the −22.65 of March 2020 came after the fall, not before.

## Twelve years of each structure

The history in this lesson has one difference from the previous ones: the time structures have legs that do not
expire. The rule: on the session after each monthly expiration the structure is opened with the next expiration
(short) and the one after (long), at the bid and at the ask; at the short expiration, the short legs settle at their
intrinsic value and the long ones are sold at that day's bid, with the actual chain (what they were really worth, with
whatever volatility they had). Single-expiration structures are settled as in 3.5.1. When the strike of a long leg is
not listed in its expiration, the closest one is taken.

```python
def cerca(d, K):
    """The listed strike closest to K (the grids of two
    expirations do not always match)."""
    ks = d.strike.unique()
    return float(ks[np.abs(ks - K).argmin()])


def cierre(d, tipo, K, n):
    """Price to close a leg: sell what was bought at the bid and
    buy back what was sold at the ask."""
    o = d[(d.tipo == tipo) & (d.strike == K)].iloc[0]
    return o.bid if n > 0 else o.ask


def historia(receta):
    """Opens at bid/ask on the session after an expiration and
    closes everything at the next expiration: the short legs
    settle at intrinsic value; the long ones are sold at the bid."""
    filas = []
    for f, v1, v2, d1, d2 in entradas:
        S, ST = indice.loc[f, "cierre"], indice.loc[v1, "cierre"]
        d_fin = por_dia.get((v1, v2))
        coste = final = 0.0
        for tipo, K, n, cual in receta(d1, d2):
            d = d1 if cual == 1 else d2
            K = cerca(d, K)
            o = d[(d.tipo == tipo) & (d.strike == K)].iloc[0]
            coste += n * (o.ask if n > 0 else o.bid)
            if cual == 1:
                final += n * (max(ST - K, 0) if tipo == "C"
                              else max(K - ST, 0))
            else:
                final += n * cierre(d_fin, tipo, K, n)
        filas.append({"entrada": f, "pnl": 100 * (final - coste) / S})
    return pd.DataFrame(filas).set_index("entrada")["pnl"]


sd = strike_delta
recetas = {
    "ATM calendar": lambda a, b: [
        ("C", sd(a, "C", 0.5), -1, 1), ("C", sd(a, "C", 0.5), 1, 2)],
    "double calendar 25D": lambda a, b: [
        ("P", sd(a, "P", -0.25), -1, 1), ("P", sd(a, "P", -0.25), 1, 2),
        ("C", sd(a, "C", 0.25), -1, 1), ("C", sd(a, "C", 0.25), 1, 2)],
    "diagonal 60D/25D": lambda a, b: [
        ("C", sd(a, "C", 0.25), -1, 1), ("C", sd(b, "C", 0.60), 1, 2)],
    "call ratio 1x2": lambda a, b: [
        ("C", sd(a, "C", 0.5), 1, 1), ("C", sd(a, "C", 0.25), -2, 1)],
    "put ratio 1x2": lambda a, b: [
        ("P", sd(a, "C", 0.5), 1, 1), ("P", sd(a, "P", -0.25), -2, 1)],
    "put backspread": lambda a, b: [
        ("P", sd(a, "C", 0.5), -1, 1), ("P", sd(a, "P", -0.25), 2, 1)],
    "risk reversal 25D": lambda a, b: [
        ("P", sd(a, "P", -0.25), -1, 1), ("C", sd(a, "C", 0.25), 1, 1)],
    "jade lizard 25D/10D": lambda a, b: [
        ("P", sd(a, "P", -0.25), -1, 1), ("C", sd(a, "C", 0.25), -1, 1),
        ("C", sd(a, "C", 0.10), 1, 1)],
}
hist = pd.DataFrame({k: historia(v) for k, v in recetas.items()})
pd.DataFrame({
    "media": hist.mean(), "mediana": hist.median(),
    "pct_pos": 100 * (hist > 0).mean(), "desv": hist.std(),
    "peor": hist.min(),
    "fecha_peor": hist.idxmin().dt.strftime("%Y-%m-%d"),
    "mejor": hist.max()}).round(2)
```
```output
                     media  mediana  pct_pos  desv   peor  fecha_peor  mejor
ATM calendar         -0.02    -0.04    47.89  0.59  -2.02  2020-04-20   2.04
double calendar 25D   0.08     0.09    57.04  0.73  -2.74  2020-04-20   2.17
diagonal 60D/25D      0.10     0.61    61.27  1.88  -7.58  2020-04-20   6.63
call ratio 1x2       -0.31    -0.21    40.85  1.98 -12.86  2020-07-20   2.57
put ratio 1x2         0.33    -0.00    50.00  2.38 -21.33  2020-02-24   6.75
put backspread       -0.38    -0.04    45.77  2.38  -6.88  2015-08-24  21.29
risk reversal 25D     0.76     0.36    87.32  3.82 -24.65  2020-02-24  19.57
jade lizard 25D/10D   0.22     0.81    70.42  2.55 -23.41  2020-02-24   3.87
```

*Fig. — 142 monthly trades from 2014 to 2025 (opened at the bid and ask, closed at the short expiry): mean and worst trade of each structure, in % of the index.* Two panels of horizontal bars, one row per structure. Left, the mean per trade: the risk reversal earns 0.76%, the put ratio 0.33% and the jade lizard 0.22%; the call ratio and the put backspread lose 0.31 and 0.38%; the calendars and the diagonal stay close to zero. Right, the worst trade: the risk reversal, the jade lizard and the put ratio, highlighted, lose between 21 and 25%; the call ratio 12.9%; the calendars less than 3%.

- **Calendars**: the ATM one loses 0.02% on average and wins in 48% of the months; the double calendar gains 0.08% and
  wins in 57%. Their worst entries are small (−2.0% and −2.7%), both on April 20, 2020. It is not the volatility premium
  that drives a calendar, but whether the index stays near the strike and how the term structure changes.
- **60D/25D diagonal**: it gains 0.10% on average and in 61% of the months; its worst, −7.6%, is a fall with the long
  delta of the February call.
- **1×2 call ratio**: it loses 0.31% on average, and its worst entry is that of July 20, 2020 (−12.9%): the 23.5% rise
  to expiration went through the uncovered short call. **1×2 put ratio**: it gains 0.33%, with a median of zero and a
  worst month of −21.3% (February 24, 2020). **Put backspread**: the mirror image; it loses 0.38% on average and gains
  21.3% in February 2020; its worst entry is that of August 24, 2015, the day of the last of the planted jumps of
  August 20-24: the index fell another 6.9% to expiration and ended near the long puts, the weak spot of the
  backspread, the moderate decline.
- **25D risk reversal**: the one that gains the most (0.76% on average, positive in 87% of the months), because the
  index in the data rises and the skew pays for the call. Its worst entry, −24.6%, loses almost as much as the whole
  index (−28.1% in lesson 3.5.1) even though its initial delta was half: as the index falls, the short put gains delta
  until it behaves like a full index.
- **25D/10D jade lizard**: 0.22% on average, 70% positive months, −23.4% in the worst.

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

The put ratio, the risk reversal and the jade lizard win almost always and share the worst month: that of the 28% fall
of February 2020, with losses of 21 to 25% of the index. They are the same bet in different wrapping: they sell the
left tail that the skew prices expensively. None of them is an income stream.

</aside>

2020 month by month shows how the time structures respond to a crash:

```python
hist.loc["2020-01":"2020-07", ["ATM calendar", "double calendar 25D",
                               "put ratio 1x2",
                               "put backspread"]].round(2)
```
```output
            ATM calendar  double calendar 25D  put ratio 1x2  put backspread
entrada
2020-01-21          0.11                 0.40          -0.18            0.13
2020-02-24         -0.53                -1.01         -21.33           21.29
2020-03-23         -1.09                -0.59          -1.34            1.07
2020-04-20         -2.02                -2.74          -1.07            0.85
2020-05-18          1.50                 0.03           1.82           -2.04
2020-06-22         -1.10                -1.23          -0.09           -0.05
2020-07-20         -1.60                -2.37          -0.27            0.17
```

The ATM calendar did not gain from the volatility explosion: it lost 0.53% in the February entry, 1.09% in the March
one and 2.02% in the April one. Its positive vega does not offset its negative gamma when the index moves 28% away from
the strike or rebounds 30%; and in March and April, with the term structure inverted, it bought the volatility of a
later month cheaper than the one it sold. The put ratio and the put backspread are exact opposites in February
(−21.33% and +21.29%).

The jade lizard recipe has a problem that does not show in the table:

```python
sin_riesgo = []
for f, v1, v2, d1, d2 in entradas:
    x = d1.set_index(["tipo", "strike"])
    kp, kc = sd(d1, "P", -0.25), sd(d1, "C", 0.25)
    kc2 = sd(d1, "C", 0.10)
    credito = (x.loc[("P", kp), "bid"] + x.loc[("C", kc), "bid"]
               - x.loc[("C", kc2), "ask"])
    sin_riesgo.append(credito >= kc2 - kc)
print(f"{100 * np.mean(sin_riesgo):.1f} % of entries")
```
```output
2.1 % of entries
```

Only in 2.1% of the entries did the credit exceed the width of the 25D/10D call spread. In the remaining months, the
structure in the table had upside risk: it was not a jade lizard, but a short put with a short call spread. A recipe
that chooses strikes by delta has to check the condition that defines the structure (credit greater than or equal to
the width) and, if it does not hold, change the strikes (like the 6,000/6,200/6,300 in the example, which meets it) or
not open.

## Common mistakes

- **Valuing a calendar with today's volatility.** Its result depends on the volatility of the long leg on the day the
  short one expires: 3 points change the P&L as much as its maximum gain.
- **Confusing a diagonal with a calendar.** With the long leg in the money, the diagonal is a directional position with
  negative theta.
- **Forgetting the unlimited side.** Ratios and backspreads have an uncovered end; put ones, the end of the crises.
- **Believing that a credit is free.** The put ratio and the risk reversal collect the skew; they pay for it in the
  tail.
- **Trusting the name of the structure.** The delta-based jade lizard was only one in 2.1% of the months.
- **Ignoring the bid-ask spread of structures with an almost zero premium.** 26% of the premium in the example put
  ratio.
- **American options.** With stocks, an in-the-money short leg of a calendar or a ratio can be assigned before
  expiration, especially before a dividend (lesson 3.5.5).

## Exercises

1. Build the 5,900 put calendar (sell January, buy February). Compare its cost, its maximum gain, its breakeven points
   and its Greeks with those of the 6,100 call calendar.

**Solution.**

```python
cal_put = [pata("P", 5900, -1, ENE), pata("P", 5900, 1, FEB)]
print(resumen(cal_put))
griegas(cal_put)
```
```output
{'medio': 44.7, 'real': 45.3, 'horquilla': 0.6, 'maximo': 59.68, 'minimo': -66.06, 'muertos': [5762.1, 6066.7]}
{'delta': -0.129, 'gamma': -0.00026, 'vega': 4.66, 'theta': 0.15}
```

The 5,900 put calendar costs 45.3 (less than the 6,100 call calendar, 73.9, because its strike is 3.1% out of the
money) and gains at most 59.68 if the index ends at 5,900 on January 16, with breakeven points at 5,762.1 and 6,066.7.
Its maximum loss (66.06) exceeds the debit: with the index far down, the short January put settles at its intrinsic
value and the long February one, deep in the money, is worth somewhat less than its own because of the interest rate
(the same thing seen with the in-the-money put in 3.5.2). Its Greeks are those of an out-of-the-money calendar: vega
+4.66, theta +0.15, small gamma (−0.00026) and a delta of −0.129 that the at-the-money call calendar does not have: it
needs the index to drop toward 5,900.

2. Split the entries of the ATM calendar by the slope of the term structure on the entry day (ATM IV of the long
   expiration minus that of the short one). Does the calendar gain more when the structure is in contango or when it
   is inverted?

**Solution.**

```python
pendiente = pd.Series(
    [100 * (iv_delta(d2, "C", 0.5) - iv_delta(d1, "C", 0.5))
     for f, v1, v2, d1, d2 in entradas],
    index=[e[0] for e in entradas])
cal = hist["ATM calendar"]
grupo = np.where(pendiente > 0, "contango", "inverted")
pd.DataFrame({"entradas": cal.groupby(grupo).size(),
              "pendiente_media": pendiente.groupby(grupo).mean(),
              "media": cal.groupby(grupo).mean(),
              "pct_pos": 100 * (cal > 0).groupby(grupo).mean(),
              "peor": cal.groupby(grupo).min()}).round(2)
```
```output
          entradas  pendiente_media  media  pct_pos  peor
contango        92             1.22  -0.03    46.74 -1.60
inverted        50            -1.35   0.01    50.00 -2.02
```

There is almost no difference: 92 entries in contango (average slope of +1.22 points) with an average of −0.03% and
46.7% positive months, and 50 with the structure inverted (−1.35 points) with +0.01% and 50%. The intuition (“buy the
long one when it is cheap relative to the short one”) separates nothing in this data with such a simple rule: what
decides the calendar is where the index ends relative to the strike, and the worst entry (−2.02%) falls in the
inverted group, in April 2020. With 50 and 92 observations and a standard deviation of 0.59 points, the standard error
of each average is around 0.06-0.08 points: any difference of this size is noise.

3. A colleague proposes selling the 25D risk reversal (short put and long call) every month “because over twelve years
   it has won 87% of the months and it opens for a credit”. What would you tell them?

**Solution.**

That the credit is the skew premium for taking on the left tail and that its 87% of positive months comes from an index
that rises 1% a month on average in the data: it is, above all, a long with half a delta (0.475). Its worst entry loses
24.6% of the index, like the put ratio and the jade lizard, in the same month. If they want that risk, the question is
whether the skew compensates for the tail, and that does not show in the hit rate: you have to compare the average with
that of half the index (the index gains 1.05% a month in lesson 3.5.1, so half the index would gain somewhat more than
0.5%, and the risk reversal gains 0.76%), size by the loss in a fall like that of 2020 and not by the margin of a quiet
day (lesson 3.6.1), and remember that in a crisis the skew spikes (−22.7 points in March 2020) and closing the short put
costs much more than what was collected.

## Self-assessment

1. The 6,100 call calendar (sell January, buy February) costs 73.9 points. On January 16, with the index at 6,100, it gains 28.2 if February volatility does not change and 5.7 if it drops 3 points. What position is it, above all?

   - A. A bullish directional bet
   - B. A riskless volatility sale
   - C. A purchase of the February volatility left after the January expiration (positive vega in the long leg) with the index still near the strike
   - D. A position with no Greeks, because both legs have the same strike

   *Answer C: A purchase of the February volatility left after the January expiration (positive vega in the long leg) with the index still near the strike.* Its aggregate vega is positive (3.97 points per volatility point): the long leg has more vega than the short one. Its gamma is negative: a large move in either direction takes it to the maximum loss. It gains with the index still and February volatility firm; 3 volatility points move the result by about 22 points.

2. With the same middle strike, the 6,100/6,300 1×2 call ratio costs 65.6 and the 6,100/5,900 put ratio opens for a credit of 3.7. Why?

   - A. Because of the skew: the 25D put (IV 0.1539) is much more expensive than the 25D call (IV 0.1007), and selling two puts pays for the at-the-money put
   - B. Because puts are more liquid
   - C. Because the interest rate favors puts
   - D. Because of an error in the chain

   *Answer A: Because of the skew: the 25D put (IV 0.1539) is much more expensive than the 25D call (IV 0.1007), and selling two puts pays for the at-the-money put.* The February 25D risk reversal is −5.32 points: the market (here, the generator) charges more for the left tail. The put ratio collects that excess, and that is why its risk is where the skew says the danger is: below 5,696.3 it loses without limit.

3. The 25D risk reversal (short put, long call) won in 87% of the months in the history and its worst entry lost 24.6% of the index. What is it really?

   - A. A hedge
   - B. A volatility-neutral strategy
   - C. A riskless position because it opens for a credit
   - D. A synthetic long with a delta of 0.475 financed by the skew: it gains with the rising index of the data and loses in falls as if it held half the index

   *Answer D: A synthetic long with a delta of 0.475 financed by the skew: it gains with the rising index of the data and loses in falls as if it held half the index.* Its vega is almost zero (−0.76) and its delta, 0.475: it is direction. The short put concentrates the risk in sharp falls (February 24, 2020), and the credit of 28.8 points is the skew premium for taking it on.

4. A 6,000/6,200/6,300 jade lizard (short 6,000 put, short 6,200/6,300 call spread) collects 113.7. What happens if the index rises a lot?

   - A. It loses without limit
   - B. It gains 13.7 points: the credit (113.7) exceeds the width of the call spread (100), so it has no upside risk
   - C. It loses 100 points
   - D. It loses the credit

   *Answer B: It gains 13.7 points: the credit (113.7) exceeds the width of the call spread (100), so it has no upside risk.* Above 6,300 the call spread loses its width, 100, and the credit covers that loss with 13.7 to spare. The condition (credit greater than or equal to the width) does not hold by itself: with 25D/10D strikes, in the history it only held in 2.1% of the entries. The real risk is on the downside: below 5,886.3 it loses like a short put.

5. The implied volatility of the 6,100 call is 0.0946 at 16 days and 0.1258 at 51 days. What volatility does a calendar that sells January and buys February buy?

   - A. 0.1258, that of February
   - B. The forward volatility between the two expirations, the square root of (0.1258²·51 − 0.0946²·16)/35, about 0.1377
   - C. The average, 0.1102
   - D. The difference, 0.0312

   *Answer B: The forward volatility between the two expirations, the square root of (0.1258²·51 − 0.0946²·16)/35, about 0.1377.* Total variances add up over time: that of February is that of January plus that of the January 16-February 20 stretch. Solving, the volatility of that stretch is 0.1377, higher than either implied volatility. It is the volatility that remains alive in the long option when the short one expires.

## Key points

- A calendar sells the short expiration and buys the long one: positive vega, negative gamma; it buys the forward volatility between the two expirations and gains with the index still near the strike.
- With two expirations, the P&L at the short expiration depends on the volatility of the long leg: 3 volatility points move the example calendar by about 22 points, almost the same as its maximum gain.
- Ratios and backspreads have an unlimited side; the skew makes the put ratio open for a credit and sell the left tail.
- The risk reversal measures the skew (−5.32 points in February, a median of −6.7 over twelve years, −22.7 in March 2020) and, as a structure, it is a synthetic long financed by it: it wins in 87% of the months and loses 24.6% in the worst.
- Delta-based recipes do not guarantee the structure: the 25D/10D jade lizard only had the credit above the width of the call spread in 2.1% of the entries.

## Resources

### Research

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

### Documentation

- [Cboe: options strategy benchmark indices (RXM, CLLZ…)](https://www.cboe.com/us/indices/benchmark_indices/) · cboe.com
- [Cboe: S&P 500 Index Options (SPX), contract specifications](https://www.cboe.com/tradable_products/sp_500/spx_options/specifications/) · cboe.com
- [QuantLib: documentation](https://www.quantlib.org/docs.shtml) · quantlib.org

### Books

- [Hull, J. C.: Options, Futures, and Other Derivatives (Pearson)](https://www.pearson.com/en-us/subject-catalog/p/options-futures-and-other-derivatives/P200000005938) · pearson.com

### Data

- [Level 3 data: daily options chain of the ORX index, 2014-2025 (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/opciones3_cadena.parquet) · Download · opciones3_cadena.parquet
- [Level 3 data: true ORX surface (SVI parameters and forward by day and expiration)](https://pythonparatrading.com/curso/datos/nivel-3/opciones3_verdad.parquet) · Download · opciones3_verdad.parquet

Previous lesson: [3.5.2 · Volatility and range structures: straddles, strangles, condors and butterflies](https://pythonparatrading.com/en/python-trading-course/level-3/straddles-strangles-condors-butterflies/)

Next lesson: [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/)
