---
title: "3.5.2 Straddles, strangles, condors and butterflies"
description: "Straddle, strangle, iron condor, iron butterfly, butterfly, condor and broken wing at bid/ask: Greeks, breakevens and twelve years of history."
url: "https://pythonparatrading.com/en/python-trading-course/level-3/straddles-strangles-condors-butterflies/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-3/straddles-strangles-condors-butterflies.png"
---

# 3.5.2 · Volatility and range structures: straddles, strangles, condors and butterflies

> A straddle does not bet on direction but on the size of the move, and its winged relatives cap the risk of selling it. The lesson builds the seven volatility and range structures with the chain of the ORX index, compares what they price in with what the index moved afterward and measures the worst month of each over 143 expirations.

- 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.6 · Realized vs implied volatility; volatility risk premium; VIX and VSTOXX](https://pythonparatrading.com/en/python-trading-course/level-3/realized-vs-implied-volatility-vix/), [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/)

## By the end of this lesson

- Build straddles, strangles, iron condors, iron butterflies, butterflies, condors and broken-wing butterflies from the chain, at the bid and at the ask, with their maximum, their minimum and their breakeven points.
- Read the price of a straddle as an implied move and compare it with the move realized up to expiration.
- Aggregate the Greeks of a volatility structure, relate its gamma and its theta and see how they change sign before expiration.
- Use put-call parity to recognize equivalent structures (iron butterfly and call butterfly) and choose the cheapest one to open.
- Measure the historical behavior of each structure and its worst episode, with and without the 2020 crash.

## Buying or selling movement

The structures in lesson 3.5.1 bet on a direction. Those in this one bet on the size of the move. A long **straddle**
(a call and a put with the same strike, usually at the money) gains if the index moves away from the strike by more
than it cost, up or down; a **strangle** does the same with an out-of-the-money put and call, cheaper and with the
breakeven points farther away. Sold, they gain if the index stays still.

```mermaid
flowchart LR
  C[Buy movement] --> CG[Gamma and vega +]
  CG --> CT[Pays theta]
  V[Sell movement] --> VG[Gamma and vega −]
  VG --> VT[Collects theta]
```

Both positions are a bet on the volatility that will be realized versus the one the market prices into the premium.
The seller collects the **volatility risk premium** (lesson 3.4.6): on average, implied volatility trades above the
volatility realized afterward, because buying protection against large moves has value. But the seller collects it in
exchange for keeping those large moves, and the history at the end of the lesson shows what that means.

Wings change the risk. Selling a straddle has unlimited loss; selling a straddle and buying a farther put and call is
an **iron butterfly**, with capped loss. The same turns a short strangle into an **iron condor**. By parity, each one
has a twin made only with calls (or only with puts): the **butterfly** and the **condor**. And if one of the wings is
wider than the other, the butterfly is a **broken-wing butterfly**.

```mermaid
flowchart TB
  SD[Short straddle] -- plus wings --> IB[Iron butterfly]
  SG[Short strangle] -- plus wings --> IC[Iron condor]
  IB -- parity --- MC[Call butterfly]
  IC -- parity --- CC[Call condor]
  MC -- one wing wider --> BW[Broken wing]
```

## The February 2026 chain

The data and conventions are those of lesson 3.5.1: the synthetic chain of the **ORX** index on December 31, 2025,
expiration on February 20, 2026 (51 calendar days), prices in points per unit (× 100 USD per contract), T in calendar
days / 365, continuous r and q, theta per calendar day and vega per volatility point. Each lesson of the course runs in
its own process, so the first cell repeats the functions from 3.5.1 (`bsm`, `pata`, `apertura`, `valor`, `resumen`,
`griegas`, `con_delta` and `strike_delta`); each one is explained there. Remember that the data is synthetic, with a
planted volatility premium: it serves to see the mechanics, not as proof of what happens 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")
VTO = 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
    return {"medio": round(float(medio), 2),
            "real": round(float(real), 2),
            "horquilla": round(float(real - medio), 2),
            "maximo": "unlimited" if final > 1e-9
            else round(float(pnl.max()), 2),
            "minimo": "unlimited" if final < -1e-9
            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()])
```

```python
r, q = mercado(VTO)
dias = (VTO - HOY).days
hoy = con_delta()
print(f"ORX {S0}  days {dias}  r {r:.4f}  q {q:.4f}")
x = hoy.pivot(index="strike", columns="tipo",
              values=["bid", "ask", "delta"])
x.loc[5600:6600].round(3)
```
```output
ORX 6089.45  days 51  r 0.0368  q 0.0117
           bid            ask         delta
tipo         C      P       C      P      C      P
strike
5600.0  529.50   22.5  532.60   22.9  0.893 -0.105
5700.0  438.20   30.5  440.80   31.1  0.856 -0.143
5800.0  349.80   41.5  351.90   42.1  0.806 -0.193
5900.0  265.80   56.5  267.50   57.3  0.737 -0.261
6000.0  187.90   77.8  189.20   78.7  0.645 -0.353
6100.0  118.80  108.3  119.80  109.3  0.523 -0.475
6200.0   63.60  152.4   64.50  153.6  0.372 -0.626
6300.0   27.10  215.0   27.70  216.4  0.214 -0.784
6400.0    9.30  296.4    9.70  298.1  0.096 -0.902
6600.0    0.85  486.6    0.95  489.2  0.013 -0.986
```

The February ladder around the money: strikes every 100 points, the 6,300 and 6,400 calls with deltas of 0.214 and
0.096, the 5,900 and 5,600 puts with −0.261 and −0.105. These are the legs of the examples.

## Long straddle and strangle

```python
straddle = [pata("C", 6100, 1), pata("P", 6100, 1)]
strangle = [pata("C", 6300, 1), pata("P", 5900, 1)]
for nombre, e in [("straddle 6100", straddle),
                  ("strangle 5900/6300", strangle)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
iv = straddle[0]["iv"]
coste = apertura(straddle)[1]
regla = 0.8 * iv * np.sqrt(dias / 365) * S0
print(f"implied move {100 * coste / S0:.2f} %"
      f"  rule 0.8·σ·√T·S {regla:.1f}")
```
```output
straddle 6100 {'medio': 228.1, 'real': 229.1, 'horquilla': 1.0, 'maximo': 'unlimited', 'minimo': -229.1, 'muertos': [5870.9, 6329.1]}
    {'delta': 0.049, 'gamma': 0.00278, 'vega': 18.1, 'theta': -2.23}
strangle 5900/6300 {'medio': 84.3, 'real': 85.0, 'horquilla': 0.7, 'maximo': 'unlimited', 'minimo': -85.0, 'muertos': [5815.0, 6385.0]}
    {'delta': -0.047, 'gamma': 0.0022, 'vega': 14.02, 'theta': -1.74}
implied move 3.76 %  rule 0.8·σ·√T·S 229.1
```

The 6,100 straddle costs 229.1 points at the ask (one point more than at mid) and its breakeven points are 5,870.9 and
6,329.1: the index has to move 3.76% in 51 days, in either direction, for it not to lose. That percentage is the
**implied move**, and the rule of thumb $0.8\,\sigma\sqrt{T}\,S$ (which comes from the expected value of $|Z|$ in a
normal distribution being $\sqrt{2/\pi} \approx 0.8$) gives 229.1 with the volatility of the ATM leg: the rule is
approximate, here it matches to the decimal. The 5,900/6,300 strangle costs 85.0 and needs the index to leave
5,815.0-6,385.0, 4.5% down or 4.9% up.

The Greeks summarize the position: delta almost zero (0.049; the strike is not exactly at the forward), gamma of
0.00278, vega of 18.1 points per volatility point (1,810 USD per contract) and theta of −2.23 points a day. Gamma and
theta are not independent. The Black-Scholes equation, for a delta-neutral portfolio, says that what is gained through
gamma offsets what is lost through the passage of time:

$$
\Theta \approx -\tfrac{1}{2}\,\Gamma\,\sigma^2 S^2 \quad \text{(per year; per day, divided by 365)}.
$$

```python
S = np.array([5700.0, 5900, 6000, 6100, 6200, 6300, 6500])
antes = pd.DataFrame({
    "today": valor(straddle, S, HOY) - coste,
    "in 25 days": valor(straddle, S, HOY + pd.Timedelta(days=25))
    - coste,
    "expiration": valor(straddle, S, VTO) - coste}, index=S)
antes.index.name = "ORX"
g = griegas(straddle)
theta_gamma = -0.5 * g["gamma"] * (iv * S0) ** 2 / 365
print(f"theta {g['theta']}  −½·Γ·σ²·S²/365 {theta_gamma:.2f}")
antes.round(1)
```
```output
theta -2.23  −½·Γ·σ²·S²/365 -2.23
        today  in 25 days  expiration
ORX
5700.0  170.7       163.4       170.9
5900.0   38.9        -2.6       -29.1
6000.0    5.7       -52.0      -129.1
6100.0   -0.4       -65.7      -229.1
6200.0   20.8       -41.4      -129.1
6300.0   66.7        15.3       -29.1
6500.0  212.0       185.6       170.9
```

The relationship holds to two decimal places (−2.23 and −2.23). It is the most useful way to think about a volatility
structure: whoever buys gamma pays theta and whoever sells gamma collects theta, and the balance is decided by whether
the index moves more or less than volatility implies. The table shows it in prices: with the index at 6,100 the
straddle is roughly flat today (−0.4) and loses 65.7 if the index is still there in 25 days; at 5,700 it gains 170.7
today and 163.4 in 25 days. Moving fast is worth more than moving late. At 5,700 today's P&L and the one at expiration
almost coincide (170.7 and 170.9), but not at 6,500 (212.0 and 170.9): besides time value, with r > q an in-the-money
call is worth more than its intrinsic value (the strike is paid later) and an in-the-money put, somewhat less.

## The price of the straddle versus what the index moved

How much did the index move, in the data, compared with what the straddle priced in? The next cell prepares the
monthly history used by the rest of the lesson (the same rule as in 3.5.1: open on the session after each expiration
with the next expiration, buys at the ask and sells at the bid, settlement at the expiration close, P&L in % of the
index on the entry day) and compares the price of the ATM straddle with the realized move:

```python
vtos = np.sort(cadena.vencimiento.unique())
sesiones = indice.index
por_dia = dict(tuple(cadena.groupby(["fecha", "vencimiento"])))
entradas = []                  # (session, expiration, chain)
for ant, vto in zip(vtos[:-1], vtos[1:]):
    if vto > sesiones[-1]:
        break
    f = sesiones[sesiones.searchsorted(ant, side="right")]
    if (f, vto) in por_dia:
        entradas.append((f, vto, con_delta(f, vto)))


def historia(receta):
    """Opens the structure on the session after each monthly
    expiration with the next expiration, at bid/ask, and
    settles it at the expiration close. P&L in % of the index."""
    filas = []
    for f, vto, d in entradas:
        S, ST = indice.loc[f, "cierre"], indice.loc[vto, "cierre"]
        coste = pago = 0.0
        for tipo, K, n in receta(d, S):
            o = d[(d.tipo == tipo) & (d.strike == K)].iloc[0]
            coste += n * (o.ask if n > 0 else o.bid)
            pago += n * (max(ST - K, 0) if tipo == "C"
                         else max(K - ST, 0))
        filas.append({"entrada": f, "vto": vto, "S": S, "ST": ST,
                      "coste": 100 * coste / S,
                      "pnl": 100 * (pago - coste) / S})
    return pd.DataFrame(filas).set_index("entrada")


def atm(d):
    return strike_delta(d, "C", 0.50)


def cerca(d, K):
    """The listed strike closest to K."""
    ks = d.strike.unique()
    return float(ks[np.abs(ks - K).argmin()])


mov = historia(lambda d, S: [("C", atm(d), 1), ("P", atm(d), 1)])
mov["realizado"] = 100 * (mov.ST / mov.S - 1).abs()
print(f"{len(mov)} entries; mean implied move "
      f"{mov.coste.mean():.2f} %, mean realized "
      f"{mov.realizado.mean():.2f} %")
print(f"months with realized > implied: "
      f"{100 * (mov.realizado > mov.coste).mean():.1f} %")
mov.nlargest(4, "realizado")[["coste", "realizado", "pnl"]].round(2)
```
```output
143 entries; mean implied move 3.73 %, mean realized 3.87 %
months with realized > implied: 39.2 %
            coste  realizado    pnl
entrada
2020-03-23  13.40      29.87  14.83
2020-02-24   3.08      28.24  24.83
2020-07-20   7.40      23.53  16.00
2020-04-20  10.31      19.04   9.02
```

*Fig. — Implied move (ATM straddle price at the ask, in % of the index) and realised move to expiry in the 143 monthly trades from 2014 to 2025.* Time series from 2014 to 2025. The implied-move line moves between 2 and 5% almost all the time and rises to 13.4% after the 2020 crash. The grey realised-move line sits below the line in 61% of the months, with jumps far above it in crises; the largest isolated one, highlighted, is the 24 February 2020 trade: the straddle cost 3.1% and the index moved 28.2%.

The mean implied move is 3.73% and the mean realized move, 3.87%: on average, the index moved somewhat **more** than
the straddle cost. And yet the realized move only exceeded the implied one in 39% of the months. Both things at once
are the signature of a fat tail: in most months the index moves less than priced in (the seller gains a little) and in
a few months it moves far more. The four largest are from 2020: at the February 24 entry the straddle cost 3.08% and
the index moved 28.24%; the long straddle gained 24.83% of the index. That month's volatility was not in the premium:
the implied volatility on February 24 was that of a calm market.

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

The unhedged straddle bets on |S_T − K|, which includes the drift of the index. In an index that rises 1% a month on
average, the short straddle loses through the call leg even when realized volatility is lower than implied. The “pure”
volatility premium is measured with delta hedging (lesson 3.6.2): according to the data README, the short straddle
with daily delta hedging gains 0.44% of the index per trade on average, with a worst case of −11.8%.

</aside>

## Selling the straddle and the strangle

```python
corto_straddle = [pata("C", 6100, -1), pata("P", 6100, -1)]
corto_strangle = [pata("C", 6300, -1), pata("P", 5900, -1)]
for nombre, e in [("short straddle", corto_straddle),
                  ("short strangle", corto_strangle)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
```
```output
short straddle {'medio': -228.1, 'real': -227.1, 'horquilla': 1.0, 'maximo': 227.1, 'minimo': 'unlimited', 'muertos': [5872.9, 6327.1]}
    {'delta': -0.049, 'gamma': -0.00278, 'vega': -18.1, 'theta': 2.23}
short strangle {'medio': -84.3, 'real': -83.6, 'horquilla': 0.7, 'maximo': 83.6, 'minimo': 'unlimited', 'muertos': [5816.4, 6383.6]}
    {'delta': 0.047, 'gamma': -0.0022, 'vega': -14.02, 'theta': 1.74}
```

Sold, the straddle collects 227.1 (at the bid; two points less than buying it would cost) and the strangle 83.6. They
are mirror images of the purchases: negative gamma and vega, positive theta (+2.23 and +1.74 points a day) and
unlimited loss on both sides. That is why almost nobody sells them naked except with a lot of capital and active
management; the winged version is the usual one.

## Iron condor and iron butterfly

```python
condor = [pata("P", 5800, 1), pata("P", 5900, -1),
          pata("C", 6300, -1), pata("C", 6400, 1)]
iron_bfly = [pata("P", 5900, 1), pata("P", 6100, -1),
             pata("C", 6100, -1), pata("C", 6300, 1)]
for nombre, e in [("iron condor 5800/5900/6300/6400", condor),
                  ("iron butterfly 5900/6100/6300", iron_bfly)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
```
```output
iron condor 5800/5900/6300/6400 {'medio': -33.0, 'real': -31.8, 'horquilla': 1.2, 'maximo': 31.8, 'minimo': -68.2, 'muertos': [5868.2, 6331.8]}
    {'delta': -0.05, 'gamma': -0.00068, 'vega': -3.91, 'theta': 0.41}
iron butterfly 5900/6100/6300 {'medio': -143.8, 'real': -142.1, 'horquilla': 1.7, 'maximo': 142.1, 'minimo': -57.9, 'muertos': [5957.9, 6242.1]}
    {'delta': -0.096, 'gamma': -0.00058, 'vega': -4.08, 'theta': 0.49}
```

The header figure is the 5,800/5,900/6,300/6,400 iron condor: it sells the 5,900/6,300 strangle and buys the 5,800 and
6,400 wings. It collects 31.8 points (1.2 less than at mid), gains that if the index ends between the short strikes
and loses at most 68.2 outside the wings: the width of 100 points minus the credit. Its breakeven points are at
5,868.2 and 6,331.8. Wings cost money: the short strangle collected 83.6 and the iron condor collects 31.8, but the risk
goes from unlimited to 68.2 points (6,820 USD per contract). Vega is divided by 3.6 (from −14.02 to −3.91).

The 5,900/6,100/6,300 iron butterfly sells the at-the-money straddle and buys wings 200 points away. It collects 142.1
and loses at most 57.9: its ratio between maximum gain and maximum loss is the inverse of the iron condor's, in
exchange for a narrow profit range (5,957.9-6,242.1) in which the maximum is almost never collected, because that
requires ending exactly at 6,100.

## Butterflies, condors and broken wings

The call butterfly buys a low-strike call, sells two of the middle strike and buys one of the high strike. By parity,
each call is a put plus a linear piece, and the linear pieces of the four legs cancel out: the call butterfly and the
long iron butterfly have the same payoff except for a constant (the width of the wing).

```python
mariposa = [pata("C", 5900, 1), pata("C", 6100, -2),
            pata("C", 6300, 1)]
condor_c = [pata("C", 5900, 1), pata("C", 6000, -1),
            pata("C", 6200, -1), pata("C", 6300, 1)]
for nombre, e in [("butterfly 5900/6100/6300", mariposa),
                  ("call condor 5900/6000/6200/6300", condor_c)]:
    print(nombre, resumen(e))
    print("   ", griegas(e))
ancho = 200 * np.exp(-r * dias / 365)
print(f"parity: butterfly {apertura(mariposa)[0]:.2f}  discounted"
      f" width − credit {ancho + apertura(iron_bfly)[0]:.2f}")
```
```output
butterfly 5900/6100/6300 {'medio': 55.45, 'real': 57.6, 'horquilla': 2.15, 'maximo': 142.4, 'minimo': -57.6, 'muertos': [5957.6, 6242.4]}
    {'delta': -0.096, 'gamma': -0.00058, 'vega': -4.08, 'theta': 0.51}
call condor 5900/6000/6200/6300 {'medio': 41.45, 'real': 43.7, 'horquilla': 2.25, 'maximo': 56.3, 'minimo': -43.7, 'muertos': [5943.7, 6256.3]}
    {'delta': -0.066, 'gamma': -0.00045, 'vega': -3.03, 'theta': 0.37}
parity: butterfly 55.45  discounted width − credit 55.18
```

The 5,900/6,100/6,300 butterfly costs 57.6 and gains at most 142.4; the short iron butterfly collected 142.1 and lost at
most 57.9. They are the same position, with the same Greeks (delta −0.096, vega −4.08) and breakeven points three
tenths apart. Parity is checked at mid: 55.45 for the butterfly versus 55.18 for the discounted width minus the credit
of the iron butterfly. What changes is the bid-ask spread: the butterfly, which buys an in-the-money call (5,900,
spread of 1.7 points), loses 2.15 on opening; the iron butterfly, with all its legs out of the money or at the money,
1.7. Between two equivalent structures, you open the one that costs less spread. The 5,900/6,000/6,200/6,300 call
condor separates the middle strikes: it costs 43.7, gains up to 56.3 between 6,000 and 6,200 and has its breakeven
points somewhat wider apart.

A **broken-wing butterfly** uses wings of different widths. The put version in the example buys the 6,000, sells two
5,900 and buys the 5,700: the upper wing measures 100 points and the lower one 200.

```python
bwb = [pata("P", 6000, 1), pata("P", 5900, -2), pata("P", 5700, 1)]
print("put BWB 6000/5900/5700", resumen(bwb))
print("   ", griegas(bwb))
S = np.array([5500.0, 5700, 5800, 5900, 6000, 6100, 6300])
pd.Series(valor(bwb, S, VTO) - apertura(bwb)[1], index=S,
          name="P&L at expiration").round(1)
```
```output
put BWB 6000/5900/5700 {'medio': -4.75, 'real': -3.2, 'horquilla': 1.55, 'maximo': 103.2, 'minimo': -96.8, 'muertos': [5796.8]}
    {'delta': 0.026, 'gamma': -0.00014, 'vega': -1.21, 'theta': 0.15}
5500.0    -96.8
5700.0    -96.8
5800.0      3.2
5900.0    103.2
6000.0      3.2
6100.0      3.2
6300.0      3.2
Name: P&L at expiration, dtype: float64
```

*Fig. — February 2026 5,900/6,100/6,300 call butterfly, 5,900/6,000/6,200/6,300 call condor and 6,000/5,900/5,700 broken-wing put butterfly: P&L at expiry with the opening at the bid and ask.* Three expiry P&L lines against ORX between 5,600 and 6,500. The butterfly is a triangle peaking at +142.4 at 6,100 and losing 57.6 outside 5,900-6,300. The condor is a lower trapezoid, +56.3 between 6,000 and 6,200. The broken-wing one peaks at +103.2 at 5,900, earns 3.2 points, highlighted, across the whole upside and loses 96.8 below 5,700.

It is opened for a credit of 3.2 points. If the index rises or stays above 6,000, it gains those 3.2; if it ends at
5,900, it gains 103.2; below 5,700 it loses 96.8. It is a butterfly that pays for itself in exchange for moving all the
risk to one side: the sharp fall. Its delta is almost zero today (0.026) and its vega negative (−1.21): it sells some
volatility and, above all, it sells the left tail. With such a small credit, the bid-ask spread eats a third of it (at
mid it would be 4.75).

## The iron condor before expiration

The payoff at expiration is half the story. While the position is open, its Greeks depend on where the index is and
on how much time is left, and they can change sign:

```python
credito = apertura(condor)[1]
filas = {}
for nombre, f in [("today", HOY),
                  ("in 25 days", HOY + pd.Timedelta(days=25)),
                  ("in 45 days", HOY + pd.Timedelta(days=45))]:
    for S in (5800.0, 5900, 6100, 6300, 6400):
        g = griegas(condor, S, f)
        filas[(nombre, S)] = {
            "pnl": float(valor(condor, S, f)) - credito, **g}
pd.DataFrame(filas).T.round({"pnl": 1, "delta": 3, "gamma": 5,
                             "vega": 2, "theta": 2})
```
```output
                    pnl  delta    gamma  vega  theta
today      5800.0 -12.4  0.102 -0.00023 -0.60  -0.10
           5900.0  -3.7  0.067 -0.00046 -2.07   0.10
           6100.0  -1.9 -0.057 -0.00067 -3.91   0.42
           6300.0 -24.0 -0.144 -0.00010 -1.38   0.26
           6400.0 -38.3 -0.137  0.00023  0.50   0.07
in 25 days 5800.0 -17.2  0.161 -0.00006  0.29  -0.32
           5900.0  -2.0  0.138 -0.00041 -0.92   0.03
           6100.0  12.1 -0.024 -0.00118 -3.50   0.75
           6300.0 -15.0 -0.218 -0.00033 -1.52   0.50
           6400.0 -36.9 -0.205  0.00054  1.01  -0.00
in 45 days 5800.0 -30.1  0.310  0.00075  0.87  -1.58
           5900.0   1.2  0.284 -0.00118 -0.87   0.76
           6100.0  29.9  0.027 -0.00082 -0.68   0.77
           6300.0   1.7 -0.413 -0.00256 -1.79   1.77
           6400.0 -42.9 -0.380  0.00300  1.79  -1.15
```

Today the iron condor loses a few points in any direction (−1.9 at 6,100, −24.0 at 6,300) and its whole profile is
smooth: negative gamma in the center, negative vega (−3.91 at 6,100). As the days go by, the center gains (+12.1 at 25
days, +29.9 at 45) and the edges harden. Three things that do not show in the payoff:

- **Gamma changes sign outside the short strikes.** At 45 days (six before expiration), with the index at 6,400, gamma
  is +0.00300 and vega +1.79: the long wing dominates and the structure behaves like a long call. At 6,300, on the
  short strike, gamma is −0.00256, the largest in absolute value in the whole table: there each index point moves delta
  the most, and delta is already −0.413.
- **Theta does not always work in your favor.** At 5,800 and 6,400, near the wings, theta is negative at 45 days
  (−1.58 and −1.15): the passage of time can also cost money in a premium sale.
- **Delta grows toward the sides.** Neutral today (−0.057 at 6,100), delta at 5,800 and at 6,400 goes from 0.10 and
  −0.14 today to 0.31 and −0.38 six days before expiration. Management (adjusting, closing or rolling a leg) is the
  topic of lesson 3.5.5.

## Four legs, four bid-ask spreads

```python
estructuras = {"long straddle": straddle,
               "long strangle": strangle,
               "short straddle": corto_straddle,
               "iron condor": condor, "iron butterfly": iron_bfly,
               "call butterfly": mariposa, "put BWB": bwb}
filas = []
for nombre, e in estructuras.items():
    medio, real = apertura(e)
    res = resumen(e)
    tope = res["maximo"] if res["maximo"] != "unlimited" else np.nan
    filas.append({"estructura": nombre, "patas": len(e),
                  "prima_medio": medio, "horquilla": real - medio,
                  "pct_prima": 100 * (real - medio) / abs(medio),
                  "pct_max_ganancia": 100 * (real - medio) / tope})
pd.DataFrame(filas).set_index("estructura").round(2)
```
```output
                patas  prima_medio  horquilla  pct_prima  pct_max_ganancia
estructura
long straddle       2       228.10       1.00       0.44               NaN
long strangle       2        84.30       0.70       0.83               NaN
short straddle      2      -228.10       1.00       0.44              0.44
iron condor         4       -33.00       1.20       3.64              3.77
iron butterfly      4      -143.80       1.70       1.18              1.20
call butterfly      3        55.45       2.15       3.88              1.51
put BWB             3        -4.75       1.55      32.63              1.50
```

The long straddle loses 0.44% of its premium crossing the bid-ask spread; the iron condor, 3.64% of what it collects;
the call butterfly, 3.88%; the broken-wing one, 32.6%. The more legs and the smaller the net premium, the more the
spread weighs. And the iron condor that is closed before expiration pays it twice: somewhat more than 7% of the credit
on a quiet day, before counting commissions (four per opening at a broker that charges per leg).

## Twelve years of each structure

With the same history machinery, seven structures opened every month. Strikes are chosen by delta (16D is the option
with a delta of 0.16 in absolute value; 16D for the strangle and the inner wings of the iron condor, 5D for the outer
ones) or by distance to the index (the call butterfly at ±3% and the broken-wing one with strikes at −1.5%, −3% and
−6%, the closest listed):

```python
d16 = lambda d, t, x: strike_delta(d, t, x)
recetas = {
    "long straddle": lambda d, S: [
        ("C", atm(d), 1), ("P", atm(d), 1)],
    "short straddle": lambda d, S: [
        ("C", atm(d), -1), ("P", atm(d), -1)],
    "short strangle 16D": lambda d, S: [
        ("C", d16(d, "C", 0.16), -1), ("P", d16(d, "P", -0.16), -1)],
    "iron condor 16D/5D": lambda d, S: [
        ("P", d16(d, "P", -0.05), 1), ("P", d16(d, "P", -0.16), -1),
        ("C", d16(d, "C", 0.16), -1), ("C", d16(d, "C", 0.05), 1)],
    "iron butterfly, 16D wings": lambda d, S: [
        ("P", d16(d, "P", -0.16), 1), ("P", atm(d), -1),
        ("C", atm(d), -1), ("C", d16(d, "C", 0.16), 1)],
    "call butterfly ±3%": lambda d, S: [
        ("C", cerca(d, 0.97 * S), 1), ("C", cerca(d, S), -2),
        ("C", cerca(d, 1.03 * S), 1)],
    "put BWB": lambda d, S: [
        ("P", cerca(d, 0.985 * S), 1), ("P", cerca(d, 0.97 * S), -2),
        ("P", cerca(d, 0.94 * S), 1)],
}
hist = pd.DataFrame({k: historia(v)["pnl"]
                     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
long straddle               0.11    -0.54    38.46  3.85  -8.79  2020-05-18  24.83
short straddle             -0.15     0.50    60.14  3.85 -24.86  2020-02-24   8.60
short strangle 16D         -0.10     0.64    78.32  2.79 -22.20  2020-02-24   2.39
iron condor 16D/5D          0.00     0.44    76.22  1.33  -7.36  2020-03-23   1.66
iron butterfly, 16D wings  -0.07    -0.13    46.85  1.72  -7.21  2020-04-20   6.15
call butterfly ±3%         -0.16    -0.46    37.76  0.96  -2.12  2017-06-19   2.02
put BWB                     0.11     0.07    65.73  0.65  -2.58  2025-02-24   2.05
```

*Fig. — 143 monthly trades from 2014 to 2025 (opened at the bid and ask, settled at expiry): mean and worst trade of each volatility structure, in % of the index.* Two panels of horizontal bars, one row per structure. Left, the mean per trade, between −0.16 and +0.11%: no structure wins clearly. Right, the worst trade: the short straddle, highlighted, loses 24.9% and the short strangle 22.2%; with wings, the iron condor and the iron butterfly stop at 7.4 and 7.2%; the butterflies, below 3%; the long straddle, 8.8%.

No structure clearly wins over 143 months: the averages range from −0.16% to +0.11% per entry. What sets them apart is
the shape of the distribution:

- **Long straddle**: it wins in 38% of the months, with a median of −0.54%, and its best month is February 24, 2020
  (+24.8%). Its worst month loses 8.8%: the premium paid in May 2020, with implied volatility through the roof.
- **Short straddle and strangle**: they win in 60% and 78% of the months and lose 24.9% and 22.2% in the worst, the
  February 24, 2020 entry. This is the left tail of the volatility premium: the sale collects in normal months what it
  gives back, multiplied, in one.
- **Iron condor and iron butterfly**: the wings cut the worst entry to −7.4% and −7.2%. The dates change: the worst iron
  condor is the one of the March 23, 2020 entry, when with high volatility the 16D and 5D wings were far apart and the
  30% rebound went through them on the upside (the same lesson as the credit vertical in 3.5.1).
- **Butterflies**: small, capped risk (worst entries of −2.1% and −2.6%), but the call one loses on average: it pays
  bid-ask spread and premium for a bet that only collects the maximum if the index ends right at the center. The
  broken-wing one, opened for a credit, wins in 66% of the months and its worst is the one of February 2025, when the
  index fell 6.6% to expiration and went through its wide wing.

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

Selling straddles or strangles is not an income stream. In the data they win six or seven months out of ten and their
worst month takes almost a quarter of the index notional. Any premium-selling backtest has to show its worst episode,
not only the average or the hit rate; sizing systematic selling by margin and by stress is lesson 3.6.1.

</aside>

## Common mistakes

- **Reading the hit rate as an edge.** 78% positive months coexists with a negative average: what counts is how much
  is lost when it loses.
- **Evaluating without the worst year.** Removing 2020 turns the short straddle from loser into winner (exercise 2).
- **Looking at the Greeks only today.** The gamma and theta of an iron condor change sign with price and with time.
- **Choosing the expensive structure out of two equivalent ones.** Call butterfly or iron butterfly: same position,
  different bid-ask spread.
- **Forgetting that the unhedged straddle bets on the drift.** In a rising index, the short calls lose.
- **Confusing implied move with forecast.** The price of the straddle includes the volatility risk premium and gives
  no warning of crises: the February 2020 entry cost 3.1%.

## Exercises

1. Sell a 10D strangle (5,600 put and 6,400 call) on the February chain. Compare its credit, its breakeven points and
   its Greeks with those of the iron condor in the header.

**Solution.**

```python
strangle_10 = [pata("P", 5600, -1), pata("C", 6400, -1)]
print(resumen(strangle_10))
griegas(strangle_10)
```
```output
{'medio': -32.2, 'real': -31.8, 'horquilla': 0.4, 'maximo': 31.8, 'minimo': 'unlimited', 'muertos': [5568.2, 6431.8]}
{'delta': 0.009, 'gamma': -0.00122, 'vega': -8.03, 'theta': 1.13}
```

The 10D strangle collects exactly the same as the iron condor in the header, 31.8 points, with the breakeven points
much farther away (5,568.2 and 6,431.8, 8.6% down and 5.6% up, versus 5,868.2 and 6,331.8). In exchange, its loss is
unlimited and its Greeks are more than double: vega −8.03 versus −3.91, theta +1.13 versus +0.41, gamma −0.00122 versus
−0.00068. With the same credit, the strangle wins more often and loses much more when it loses: the iron condor pays
for the 68.2-point cap on its loss with a narrow range.

2. Repeat the history table without the 2020 entries. Which structures change sign? What is the worst entry of each
   one outside 2020?

**Solution.**

```python
sin_2020 = hist[hist.index.year != 2020]
pd.DataFrame({"media": sin_2020.mean(),
              "pct_pos": 100 * (sin_2020 > 0).mean(),
              "peor": sin_2020.min(),
              "fecha_peor": sin_2020.idxmin().dt.strftime("%Y-%m-%d"),
              "media_2020": hist[hist.index.year == 2020].mean()
              }).round(2)
```
```output
                           media  pct_pos   peor  fecha_peor  media_2020
long straddle              -0.25    38.17  -5.25  2018-03-19        4.11
short straddle              0.22    60.31 -10.37  2021-04-19       -4.20
short strangle 16D          0.21    80.15  -9.23  2021-04-19       -3.58
iron condor 16D/5D          0.13    77.86  -4.24  2018-01-22       -1.41
iron butterfly, 16D wings  -0.02    47.33  -3.99  2018-11-19       -0.67
call butterfly ±3%         -0.15    38.17  -2.12  2017-06-19       -0.29
put BWB                     0.10    64.89  -2.58  2025-02-24        0.19
```

Without 2020 four change sign: the long straddle (from +0.11% to −0.25%), the short straddle (from −0.15% to +0.22%),
the short strangle (from −0.10% to +0.21%) and the iron condor (from 0.00% to +0.13%). The twelve months of 2020 are
worth +4.11% on average for the long straddle and −4.20% for the short one. Outside 2020, the worst entries of the
naked sales are those of April 19, 2021 (−10.4% and −9.2%): a rally that went through the short call, the right tail.
Those of the winged structures, from 2018 (−4.2% and −4.0%). The butterflies do not change: their worst month was not
in 2020. The lesson is not that 2020 is an anomaly to be removed, but that the average of a volatility sale depends on
whether the sample contains a crisis.

3. A 16D/5D iron condor and a put broken-wing butterfly have similar averages in the history (0.00% and 0.11%) and
   worst entries of −7.4% and −2.6%. Does that mean the broken-wing butterfly is better?

**Solution.**

Not necessarily. These are figures from a single history of 143 entries: with monthly standard deviations of 0.65% and
1.33%, the standard error of each average is 0.05 and 0.11 points, of the same order as the average itself. The two
structures, moreover, do not risk the same amount nor in the same way. The maximum loss of the delta-based iron condor
grows with volatility (its wings spread apart); that of the broken-wing butterfly is set by its widths in % of the
index rounded to the listed strikes, and that rounding changes the structure. On February 24, 2020 the recipe gave
2,900/2,850/2,800: equal wings, a regular butterfly that lost 0.13% with the index 28% lower. On February 24, 2025 it
gave 5,450/5,400/5,200: a lower wing four times wider, which lost 2.58% with a 6.6% fall. To compare, you have to
equalize the maximum risk per entry and check which structure was actually opened each month, not rely on two figures.

## Self-assessment

1. The February 6,100 straddle costs 229.1 points with the index at 6,089.45. What is that price saying?

   - A. That the market expects the index to rise 3.76%
   - B. That implied volatility is 3.76%
   - C. That the straddle gains if the index does not move
   - D. That, to avoid a loss, the index has to end outside 5,870.9-6,329.1: a move of 3.76% in either direction

   *Answer D: That, to avoid a loss, the index has to end outside 5,870.9-6,329.1: a move of 3.76% in either direction.* The long straddle loses the whole premium if the index ends at the strike and gains whatever it moves away from it minus what was paid. Its breakeven points are 6,100 ± 229.1, and 229.1 / 6,089.45 = 3.76% is the implied move. The rule of thumb (0.8 times volatility, times the square root of T and times the index, with volatility 0.1258 and T = 51/365) gives the same.

2. A short 5,900/6,100/6,300 iron butterfly collects 142.1 and a long 5,900/6,100/6,300 call butterfly costs 57.6. What is the relationship between them?

   - A. They are the same position by parity: the payoff of the butterfly is that of the iron butterfly plus a constant, and its mid price satisfies that the butterfly is worth the discounted width minus the credit of the iron butterfly
   - B. None: one is built with puts and calls and the other only with calls
   - C. The butterfly has positive vega and the iron butterfly negative
   - D. The iron butterfly has unlimited risk

   *Answer A: They are the same position by parity: the payoff of the butterfly is that of the iron butterfly plus a constant, and its mid price satisfies that the butterfly is worth the discounted width minus the credit of the iron butterfly.* Both gain the most at 6,100 and lose the same outside 5,900-6,300. At mid, 55.45 versus 200·e^(−rT) − 143.8 = 55.18: parity holds within the bid-ask spreads. They have the same Greeks; you choose the one that costs less spread to open.

3. The short iron condor has negative gamma today at 6,100. Six days before expiration, with the index at 6,400 (the long call wing), its gamma is positive. Why?

   - A. Because of a rounding error
   - B. Because near expiration gamma concentrates at the strike closest to the index, and at 6,400 that strike is the long call's
   - C. Because implied volatility has risen
   - D. Because the iron condor always has positive gamma

   *Answer B: Because near expiration gamma concentrates at the strike closest to the index, and at 6,400 that strike is the long call's.* The gamma of an at-the-money option grows sharply as expiration approaches and that of the others vanishes. With the index at the long wing, the gamma of the structure is that of that long call: +0.00300 at 45 days after opening. That is why the Greeks of a structure have to be looked at across several prices and dates, not only today.

4. In the 2014-2025 history, the short straddle wins in 60% of the months but its average is −0.15% per entry. Without 2020, its average is +0.22%. What does it teach?

   - A. That 2020 is wrong data and should be removed from the backtest
   - B. That the short straddle is a riskless strategy outside crises
   - C. That the average of a volatility sale is decided by very few months: those of 2020 lose 4.2% on average each and wipe out eleven years of gains
   - D. That the volatility premium does not exist in the data

   *Answer C: That the average of a volatility sale is decided by very few months: those of 2020 lose 4.2% on average each and wipe out eleven years of gains.* Removing the bad year turns a losing strategy into a winning one: it is the most dangerous bias when evaluating premium selling. The worst entry (February 24, 2020) loses 24.9% of the index; even without 2020, the worst one (April 2021, the call leg in a rally) loses 10.4%.

5. The 6,000/5,900/5,700 put broken-wing butterfly is opened for a credit of 3.2 points. What risk does it carry?

   - A. It gains 3.2 if the index rises or stays put; it gains up to 103.2 if it ends at 5,900; it loses up to 96.8 if it ends below 5,700
   - B. None, because it collects at opening
   - C. It loses if the index rises
   - D. It loses at most the credit

   *Answer A: It gains 3.2 if the index rises or stays put; it gains up to 103.2 if it ends at 5,900; it loses up to 96.8 if it ends below 5,700.* The lower wing is twice as wide as the upper one (200 versus 100 points): that finances the butterfly and removes the upside risk, but leaves a loss of 200 − 100 − 3.2 = 96.8 points below 5,700. The bid-ask spread, moreover, eats a third of the credit: at mid it would be 4.75 points.

## Key points

- A straddle or a strangle buys or sells movement, not direction: the price of the ATM straddle is the implied move to expiration, about 0.8 times volatility times the square root of the time.
- Gamma and theta go together: theta is approximately −½ × gamma × (volatility × index)², per year; whoever collects theta sells gamma and whoever pays theta buys it.
- Wings turn a short straddle or strangle with unlimited risk into an iron butterfly or iron condor with a known maximum loss; parity makes them equivalent to call butterflies and condors.
- With four legs the bid-ask spread weighs: the iron condor in the example loses 3.6% of its credit on opening and the broken-wing butterfly, a third.
- The history of movement sales is decided by the tail: the short straddle wins in 60% of the months, loses 24.9% in the worst and its average goes from −0.15% to +0.22% if 2020 is removed. Wings cut the worst entry to −7.4%.

## Resources

### Research

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

### Documentation

- [Cboe: options strategy benchmark indices (BFLY, CNDR…)](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

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

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

Next lesson: [3.5.3 · Calendars, diagonals, ratios, backspreads, risk reversals and jade lizard](https://pythonparatrading.com/en/python-trading-course/level-3/calendars-diagonals-ratios-risk-reversals/)
