---
title: "3.5.5 Managing option positions"
description: "Adjustments, rolls, early closes, early assignment with American options and dividends, and pin risk at expiration, measured on option data."
url: "https://pythonparatrading.com/en/python-trading-course/level-3/managing-option-positions/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-3/managing-option-positions.png"
---

# 3.5.5 · Management: adjustments, rolls, early closes, early assignment and pin risk

> Opening an option position is half of the trade; the other half is the decisions while it lives: close it early, roll it, adjust it or let it expire. This lesson measures five management rules for a put-selling strategy on the level's synthetic data, shows what a roll is and what it is not, when it pays to exercise an American option before expiration and why a close pinned to the strike is a risk.

- 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: 65 min
- Prerequisites: [3.4.2 · Binomial trees, Black-Scholes-Merton, Black-76 and Monte Carlo](https://pythonparatrading.com/en/python-trading-course/level-3/binomial-black-scholes-black-76-monte-carlo/), [3.4.3 · Greeks: delta, gamma, vega, theta, rho, vanna, volga and charm](https://pythonparatrading.com/en/python-trading-course/level-3/first-and-second-order-option-greeks/), [3.5.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.4 · A custom tool: payoff, aggregate Greeks and scenarios](https://pythonparatrading.com/en/python-trading-course/level-3/option-payoff-greeks-scenario-tool/)

## By the end of this lesson

- Measure management rules (close on profit, on time and on loss) on a systematic put-selling strategy, buying back at the ask, and recognize which trades decide the comparison.
- Compute a roll as what it is: a close and an open, and separate its result from that of the original trade.
- Identify with American option data when early exercise is optimal (calls before the ex-date, deep in-the-money puts) and what it means for the short side.
- Quantify pin risk: how many expirations close pinned to a strike and what stock position may appear on Monday.

## A live position: what can be decided

Between the opening and the expiration of an option position there are four possible decisions, and each day that
passes one of them is taken, even if it is doing nothing:

```mermaid
stateDiagram-v2
  [*] --> Open
  Open --> Adjusted: adjustment
  Adjusted --> Open
  Open --> Closed: close
  Open --> Rolled: roll
  Rolled --> Open: new position
  Open --> Expired: expiration
  Open --> Assigned: exercise
  Closed --> [*]
  Expired --> [*]
  Assigned --> [*]
```

| Decision | What it is | What changes in the Greeks | Cost |
|---|---|---|---|
| Let it expire | Do nothing until expiration | Nothing; gamma grows as expiration nears | None, except settlement or delivery |
| Close early | Buy back what was sold (or sell what was bought) | Everything to zero | Exit bid-ask spread |
| Roll | Close and open another position further out in time or in strike | Those of the new position | Two bid-ask spreads |
| Adjust | Add or remove legs (wings, hedges with the underlying, change a strike) | The ones you want to correct | One or more bid-ask spreads |
| Assignment or exercise | The counterparty exercises (American options) or expiration arrives in the money | Option replaced by underlying or cash | Lost dividend, delivery, financing |

The lesson uses two synthetic data sets from the level. The ORX options (lessons 3.5.4 and 3.6.1) are European and
cash settled: they serve to measure closing and rolling rules. Those of KRN, a fictitious stock with a quarterly
dividend, are American and priced with a binomial tree with discrete dividends (lesson 3.4.2 and the data README):
they serve for early exercise and pin risk. The results on the data do not say which rule works in a real market;
they show how to measure it and where the traps are.

## Closing early: five rules measured on ORX

The base strategy is simple: on the day of each monthly expiration, a −0.16 delta put of the next expiration (with 20
days or more) is sold at the bid and managed with a rule. It is the put leg of the strangle in lesson 3.6.1, without
leverage: the result is measured in % of each trade's strike.

```python
import numpy as np
import pandas as pd
from scipy.special import ndtr

ind = pd.read_csv("opciones3_indice.csv", parse_dates=["fecha"])
ind = ind.set_index("fecha")
S, r = ind["cierre"], ind["tipo_1m"] / 100
q = ind["rentabilidad_dividendo"] / 100
cad = pd.read_parquet(
    "opciones3_cadena.parquet",
    columns=["fecha", "vencimiento", "tipo", "strike", "bid", "ask",
             "iv_modelo"])
puts = cad[cad["tipo"] == "P"]
por_dia = dict(tuple(puts.groupby("fecha")))
series = {k: g.set_index("fecha")[["bid", "ask"]]
          for k, g in puts.groupby(["vencimiento", "strike"])}
vencs = sorted(puts["vencimiento"].unique())
print(len(por_dia), "days with a chain ·", len(vencs), "expirations")
```
```output
3017 days with a chain · 147 expirations
```

```python
def delta_bsm(tipo, S, K, T, iv, r, q):
    d1 = (np.log(S / K) + (r - q + iv**2 / 2) * T) / (iv * np.sqrt(T))
    w = 1.0 if tipo == "C" else -1.0
    return w * np.exp(-q * T) * ndtr(w * d1)


def put_por_delta(f, v, objetivo):
    """Put of expiration v with |delta| closest to the target."""
    g = por_dia[f]
    g = g[g["vencimiento"] == v]
    T = (v - f).days / 365
    d = delta_bsm("P", S[f], g["strike"].to_numpy(), T,
                  g["iv_modelo"].to_numpy(), r[f], q[f])
    return g.iloc[int(np.argmin(np.abs(np.abs(d) - objetivo)))]


def ciclo(objetivo=0.16):
    """Sells a put at the bid on the day of each monthly expiration
    (or the first day with a chain), of the next expiration with
    20 days or more. Returns one trade per cycle."""
    ops, f = [], S.index[0]
    while True:
        while f not in por_dia:
            f = S.index[S.index.get_loc(f) + 1]
        v = next((x for x in vencs if (x - f).days >= 20), None)
        if v is None or v > S.index[-1]:
            return ops
        fila = put_por_delta(f, v, objetivo)
        k = fila["strike"]
        ops.append({"entrada": f, "venc": v, "strike": k,
                    "credito": fila["bid"], "ST": S[v],
                    "cot": series[(v, k)].loc[f:v].iloc[1:]})
        f = v


ops = ciclo(0.16)
cred = pd.Series([100 * o["credito"] / o["strike"] for o in ops])
print(len(ops), "trades · average credit",
      f"{cred.mean():.2f}% of the strike")
pd.DataFrame(ops[:3]).drop(columns="cot")
```
```output
143 trades · average credit 0.65% of the strike
     entrada       venc  strike  credito       ST
0 2014-01-02 2014-02-21  1700.0      7.5  1882.25
1 2014-02-21 2014-03-21  1800.0      6.7  1839.01
2 2014-03-21 2014-04-17  1750.0      6.7  1793.62
```

There are 143 trades between 2014 and 2025, with an average credit of 0.65% of the strike: about 0.65 points per 100
of notional, collected in exchange for covering drops below a strike that sits, at the median, 5.8% below the index
(between 4.1 and 9.5% in the central 80% of trades). The rules:

- **Hold**: settlement at expiration, doing nothing.
- **50%**: buy back as soon as the put costs half of the credit (the most cited profit target in premium selling).
- **21 days**: buy back when 21 calendar days remain, before gamma grows.
- **Stop 2×**: buy back if the put becomes worth three times the credit (a loss of two credits).
- **50% or 21 days**: whichever comes first.

All of them buy back at the ask of the close of the day the condition is met: it is what can be done with daily data,
and it is less favorable than an order during the session.

```python
def gestiona(op, objetivo=None, dias=None, stop=None):
    """P&L per unit (points) and exit date. Buys back at the ask
    at the close of the first day a rule is met; otherwise,
    settlement at expiration."""
    for f, (bid, ask) in op["cot"].iterrows():
        if f >= op["venc"]:
            break
        if objetivo is not None and ask <= objetivo * op["credito"]:
            return op["credito"] - ask, f
        if stop is not None and ask >= stop * op["credito"]:
            return op["credito"] - ask, f
        if dias is not None and (op["venc"] - f).days <= dias:
            return op["credito"] - ask, f
    pago = max(op["strike"] - op["ST"], 0.0)
    return op["credito"] - pago, op["venc"]


REGLAS = {"hold": {}, "50%": {"objetivo": 0.5},
          "21 days": {"dias": 21}, "stop 2x": {"stop": 3.0},
          "50% or 21 days": {"objetivo": 0.5, "dias": 21}}


def resultados(ops):
    pnl, dias = {}, {}
    for nombre, kw in REGLAS.items():
        filas = [gestiona(o, **kw) for o in ops]
        pnl[nombre] = pd.Series(
            [100 * x / o["strike"] for (x, _), o in zip(filas, ops)],
            index=[o["entrada"] for o in ops])
        dias[nombre] = np.mean([(f - o["entrada"]).days
                                for (_, f), o in zip(filas, ops)])
    pnl = pd.DataFrame(pnl)  # % of the strike per trade
    tabla = pd.DataFrame({
        "media": pnl.mean(), "mediana": pnl.median(),
        "ganadoras": (pnl > 0).mean(), "peor": pnl.min(),
        "dias": pd.Series(dias), "total": pnl.sum()})
    return pnl, tabla


pnl, tabla = resultados(ops)
tabla.round(2)
```
```output
                media  mediana  ganadoras   peor   dias  total
hold             0.29     0.55       0.95 -24.97  30.55  41.18
50%              0.17     0.33       0.98 -24.97  11.41  23.80
21 days          0.23     0.25       0.85  -2.10   9.66  33.06
stop 2x          0.46     0.55       0.93  -3.53  29.78  65.75
50% or 21 days   0.22     0.24       0.87  -2.10   7.91  31.46
```

The table is the summary usually shown to defend a rule, and each column tells a different story. Closing at 50%
raises winning trades from 95 to 98% and cuts the holding time from 30.6 to 11.4 days, but lowers the mean from 0.29
to 0.17% per trade and keeps the worst one, −24.97%. The 21-day and stop rules trim the worst trade to −2.10 and
−3.53%, and the stop adds up to 65.75 versus 41.18 for holding. Read this way, the stop would seem the best rule. But
the table mixes 140 normal months with three episodes:

```python
peores = pnl["hold"].nsmallest(3).index
print("Worst with “hold”:",
      [f.date().isoformat() for f in peores])
pd.DataFrame({"total": pnl.sum(),
              "without the 3 worst": pnl.drop(peores).sum(),
              "in those 3": pnl.loc[peores].sum()}).round(2)
```
```output
Worst with “hold”: ['2020-02-21', '2018-01-19', '2018-09-21']
                total  without the 3 worst  in those 3
hold            41.18                78.29      -37.12
50%             23.80                48.29      -24.49
21 days         33.06                33.20       -0.15
stop 2x         65.75                72.99       -7.24
50% or 21 days  31.46                31.60       -0.15
```

The header figure separates the two parts. The three worst trades of “hold” are those opened in February 2020,
January 2018 and September 2018: in them, holding loses a total of 37.1 percentage points of the strike, and any rule
that exits earlier loses less. In the other 140, holding is the one that adds up the most (78.3), the stop comes close
(73.0) and the time rules stay at 33: closing at 21 days gives up half of each month's theta. With 143 cycles, three
decide the comparison between rules. If the next crisis arrives in a gap and not over several sessions, or in the
second half of the cycle instead of the first, the ranking may change.

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

A management rule does not create an edge: it changes the shape of the result. The profit target turns a
distribution with many small wins into another with more and smaller wins, without touching the tail. The time and
loss rules trade profit in normal months for protection in bad ones. Which one suits you depends on how much the tail
weighs on your capital (lesson 3.6.1), not on the hit rate.

</aside>

The stop also has a problem of its own: it is checked at the close and filled where the market is, not where it was
placed.

```python
salidas = []
for o in ops:
    x, f = gestiona(o, stop=3.0)
    if f < o["venc"]:
        salidas.append({"entrada": o["entrada"].date(),
                        "salida": f.date(),
                        "perdida_en_creditos": -x / o["credito"]})
pd.DataFrame(salidas).round(2)
```
```output
      entrada      salida  perdida_en_creditos
0  2014-02-21  2014-02-27                 2.12
1  2016-06-17  2016-06-24                 2.59
2  2018-01-19  2018-02-02                 2.03
3  2018-09-21  2018-10-18                 6.82
4  2020-02-21  2020-03-09                 6.88
5  2022-06-17  2022-07-07                 2.39
6  2025-02-21  2025-02-28                 2.24
7  2025-03-21  2025-04-04                 4.21
```

Of eight stops, five fill at a loss of between 2.03 and 2.59 credits, close to the planned 2. Those of the trades
opened in September 2018, February 2020 and March 2025 fill at 6.82, 6.88 and 4.21 credits: in a single session the
put went from being worth less than three credits to being worth more than five, or almost eight. The 2020 one
triggers on 9 March, the day of the −7.9% in the README: a stop checked at the close arrives after the gap. And an
intraday stop, on an option price with bid-ask spreads that widen under stress, triggers more easily on noise.

## Rolling and adjusting

Rolling is closing a position and opening a similar one further out in time, at another strike or both. It is usually
presented as a way to “give time” to a losing trade. The numbers in the data show what it is: two trades. The 2525
put sold on 2018-01-19 goes in the money on 5 February, the day of the −4.2% gap in the README; it is bought back at
the ask and a −0.16 delta put of the next expiration is sold at the bid:

```python
o = next(o for o in ops if o["entrada"] == pd.Timestamp("2018-01-19"))
tocada = next(f for f in o["cot"].index if S[f] < o["strike"])
cierre = o["cot"].at[tocada, "ask"]
nueva = put_por_delta(tocada, next(
    x for x in vencs if (x - tocada).days >= 20 and x > o["venc"]),
    0.16)
v2, k2 = nueva["vencimiento"], nueva["strike"]
pago2 = max(k2 - S[v2], 0.0)
print(f"Sold {o['entrada'].date()}: put {o['strike']:.0f} at "
      f"{o['credito']}; tested on {tocada.date()} with ORX {S[tocada]}")
print(f"Buyback at the ask {cierre}; new put {k2:.0f} "
      f"({v2.date()}) at the bid {nueva['bid']}")
pd.Series({
    "hold": o["credito"] - max(o["strike"] - o["ST"], 0),
    "close when tested": o["credito"] - cierre,
    "roll (close + new)": (o["credito"] - cierre
                           + nueva["bid"] - pago2),
    "  of which, the new one": nueva["bid"] - pago2,
}).round(2)
```
```output
Sold 2018-01-19: put 2525 at 9.9; tested on 2018-02-05 with ORX 2440.24
Buyback at the ask 106.5; new put 2150 (2018-03-16) at the bid 30.2
hold                      -194.87
close when tested          -96.60
roll (close + new)         -66.40
  of which, the new one     30.20
dtype: float64
```

Held, the put sold at 9.9 points loses 194.87. Closed when tested, 96.60. The roll adds up to −66.40, but that figure
merges two trades: the first lost 96.60, realized at the buyback; the second, a March 2150 put sold at 30.2 with
volatility soaring, expired worthless and earned 30.20. That the second one went well has nothing to do with the
first: it could have been opened without the first, and it would have earned the same. Merging them into a single P&L
makes it look as if the roll recovered part of the loss.

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

The roll “for a credit” (buying back and selling another option that collects more than the buyback costs) forces you
to sell more risk: a closer strike, a later expiration or more contracts. Repeated after each loss, it is a martingale
with options: the position grows just when the market goes against you. It is evaluated as a new trade, with the size
and risk it would have if the previous one did not exist.

</aside>

Adjustments are variants of the same thing: adding or removing legs to change the Greeks. With the tool from lesson
3.5.4 they can be computed before making them:

| Adjustment | Example | Main effect |
|---|---|---|
| Buy a wing | Turn a short put into a put spread | Maximum loss capped; less theta and less short vega |
| Hedge the delta | Sell or buy the underlying or futures | Delta to zero; gamma and vega unchanged |
| Roll the untested leg | In a strangle after a drop, raise the short call | Recenters the delta collecting a bit more premium; increases risk if the market rebounds |
| Reduce size | Buy back part of the contracts | All the Greeks proportionally; realizes part of the loss |

## Early assignment: American options with dividends

Options on US stocks are American: the holder can exercise them any day. Whoever is short them can be assigned
before expiration. The clearing house (OCC) allocates assignments among its members and each broker among its clients
with a fixed method, FIFO or random, which it must disclose in writing (FINRA Rule 2360). To know when it happens, you
need to know when it suits the holder.

An American option is worth at least its intrinsic value. If it is worth exactly that, exercising now is optimal:
waiting adds nothing. For each option, the KRN chain has the American price (`precio_modelo`, a 150-step binomial tree
with discrete dividends) and that of the same European option (`europea_modelo`). The difference is the **early
exercise premium**:

```python
krn = pd.read_csv("accion3_diario.csv", parse_dates=["fecha"])
krn = krn.set_index("fecha")["cierre"]
ev = pd.read_csv("accion3_eventos.csv", parse_dates=["fecha"])
divs = ev[ev["evento"] == "dividendo"].set_index("fecha")["dividendo"]
op_a = pd.read_parquet("accion3_opciones.parquet")
op_a["S"] = krn.reindex(op_a["fecha"]).to_numpy()
w = np.where(op_a["tipo"] == "C", 1.0, -1.0)
op_a["intr"] = np.maximum(w * (op_a["S"] - op_a["strike"]), 0)
op_a["valor_temporal"] = op_a["precio_modelo"] - op_a["intr"]
op_a["prima_ejercicio"] = (op_a["precio_modelo"]
                           - op_a["europea_modelo"])
# price = intrinsic (± the rounding of the close and of the tree):
# exercising now or at the next opportunity is the same
op_a["ejercer"] = op_a["valor_temporal"] < 0.02
dentro = op_a[op_a["intr"] > 0]
dentro.groupby("tipo")[["prima_ejercicio", "ejercer"]].agg(
    ["mean", "max"]).round(3)
```
```output
     prima_ejercicio        ejercer
                mean    max    mean   max
tipo
C              0.188  1.019   0.022  True
P              0.335  2.348   0.229  True
```

Among in-the-money options, the average early exercise premium is 0.19 USD for calls and 0.34 for puts, with maxima
of 1.02 and 2.35 USD. The cell considers exercise optimal when the American price exceeds the intrinsic value by less
than 2 cents: the close is rounded to the cent and the tree, with steps of a few hours, leaves a cent or so of value
for exercising at the next step. With that criterion, exercising now is optimal in 2.2% of the in-the-money call rows
and in 22.9% of the put rows. They are two different mechanisms.

### Calls: the dividend

A call does not pay a dividend; the stock does. On the eve of the ex-date, the holder of an in-the-money call compares
two things: if they exercise, they receive the stock and collect the dividend $D$; if not, the stock drops by $D$ the
next day and they keep the option's time value. By put-call parity, that time value is approximately that of the put
with the same strike plus the interest on the strike until expiration. Exercising can only pay if

$$
D > P(K) + K\left(1 - e^{-r\tau}\right)
$$

with $\tau$ the time from the ex-date to expiration. The cell checks it against the tree on every ex-date eve in the
data:

```python
sesiones = krn.index
vispera = {sesiones[sesiones.get_loc(d) - 1]: d for d in divs.index}
c = dentro[(dentro["tipo"] == "C")
           & dentro["fecha"].isin(list(vispera))].copy()
c["ex"] = c["fecha"].map(vispera)
c = c[c["vencimiento"] >= c["ex"]]
c["D"] = c["ex"].map(divs)
eu_put = op_a[op_a["tipo"] == "P"].set_index(
    ["fecha", "vencimiento", "strike"])["europea_modelo"]
c["put"] = eu_put.reindex(
    pd.MultiIndex.from_frame(c[["fecha", "vencimiento", "strike"]])
).to_numpy()
tau = (c["vencimiento"] - c["ex"]).dt.days / 365
r_c = ind["tipo_1m"].reindex(c["fecha"]).to_numpy() / 100
# what is lost by exercising: the put's insurance and the interest
# on the strike until expiration
c["umbral"] = c["put"] + c["strike"] * (1 - np.exp(-r_c * tau))
c["regla"] = c["D"] > c["umbral"]
print(len(c), "in-the-money calls on the eves")
print(pd.crosstab(c["ejercer"], c["regla"],
                  rownames=["tree: exercise"], colnames=["rule"]))
c[c["ejercer"]].groupby(c["fecha"].dt.year).size()
```
```output
494 in-the-money calls on the eves
rule            False  True
tree: exercise
False             324     66
True                0    104
fecha
2022    46
2023    22
2024    14
2025    22
dtype: int64
```

Of 494 in-the-money calls on the eves, the tree exercises 104 and the rule flags those 104 and 66 more. It is a
necessary condition, not a sufficient one: in the remaining 66 the tree still finds time value (the approximation
ignores the volatility left until expiration, beyond the put's insurance). Optimal exercises appear every year (46 in
2022, 22 in 2023, 14 in 2024 and 22 in 2025) and always in the series that expires one or two weeks after the
ex-date: the less time remains, the less time value is sacrificed. An example, the 95 call expiring on 2022-02-18 with
the stock at 106-110 USD:

```python
ej = op_a[(op_a["tipo"] == "C") & (op_a["strike"] == 95)
          & (op_a["vencimiento"] == pd.Timestamp("2022-02-18"))]
ej = ej.set_index("fecha").loc["2022-01-27":"2022-02-17"]
ej = ej.assign(europea_menos_intr=ej["europea_modelo"] - ej["intr"])
ej[["S", "precio_modelo", "intr", "valor_temporal",
    "europea_menos_intr"]].iloc[[0, 5, 8, 9, 10, 12]].round(3)
```
```output
                 S  precio_modelo   intr  valor_temporal  europea_menos_intr
fecha
2022-01-27  109.79         14.806  14.79           0.016              -0.821
2022-02-03  110.43         15.436  15.43           0.006              -0.850
2022-02-08  106.78         11.779  11.78          -0.001              -0.847
2022-02-09  106.84         11.835  11.84          -0.005              -0.857
2022-02-10  106.62         11.639  11.62           0.019               0.019
2022-02-14  105.60         10.606  10.60           0.006               0.006
```

*Fig. — KRN 95 call expiring 2022-02-18 (stock at 106-110 USD): price minus intrinsic value of the American and of the European before and after the 2022-02-10 ex-date (0.88 USD dividend).* Two lines from 2022-01-21 to 2022-02-17. The American is worth almost exactly its intrinsic value: +0.04 USD at first and 0 in the sessions before the ex-date (exercising is optimal). The European (highlighted) is worth 0.78 to 0.86 USD less than intrinsic before the ex-date, almost the 0.88 dividend it will lose, and from 10 February it matches the American.

The American option is worth its intrinsic value plus 0.016 USD on 27 January and practically nothing in the week
before the ex-date (0.006 on 3 February; −0.001 and −0.005 on the two eves, due to the rounding of the close). The
European one is worth between 0.82 and 0.86 USD less than its intrinsic value: it cannot be exercised and will lose
the 0.88 dividend. From 10 February, with no pending dividends, the two coincide. Whoever is short that call must
expect to be assigned on the eve: they will deliver 100 shares per contract and, if they did not own them, they will
be short on the ex-date and owe the dividend. The diagram summarizes the check a call seller makes before each ex-date
(TV is the call's time value, D the dividend):

```mermaid
flowchart TB
  A{Short ITM call} --> B{Ex-date before}
  B -- no --> C[Low risk]
  B -- yes --> D{TV less than D}
  D -- no --> C
  D -- yes --> E[Expect assignment]
  E --> F[Close or roll]
```

### Puts: the interest on the strike

A deep in-the-money put is almost a forward sale of the stock at price K. Exercising it brings forward the receipt of
K, which earns interest; waiting keeps an insurance against the stock rising back above K, which is worth little if
the put is deep in the money. With positive rates, beyond a certain distance exercising is optimal:

```python
p = dentro[dentro["tipo"] == "P"].copy()
p["dentro_pct"] = 100 * (p["strike"] / p["S"] - 1)
tramo = pd.cut(p["dentro_pct"], [0, 2, 5, 10, 20, 60])
p.groupby(tramo, observed=True).agg(
    filas=("ejercer", "size"), ejercer=("ejercer", "mean"),
    prima_media=("prima_ejercicio", "mean")).round(3)
```
```output
            filas  ejercer  prima_media
dentro_pct
(0, 2]       3594    0.001        0.065
(2, 5]       5412    0.034        0.099
(5, 10]      8991    0.122        0.184
(10, 20]     9438    0.337        0.377
(20, 60]     7396    0.476        0.767
```

Near the money, exercise is almost never optimal (0.1% of rows). More than 10% in the money it is optimal in a third
of the rows (33.7%), and more than 20% in almost half (47.6%). The early exercise premium grows with the distance,
from 0.07 to 0.77 USD on average. For whoever sells puts on stocks, a sharp drop brings a likely early assignment: the
purchase of 100 shares per contract earlier than planned, which has to be financed.

## Pin risk at expiration

In physically settled options, on expiration day those that finish in the money are exercised automatically unless
there is a contrary instruction (the exercise-by-exception procedure of OCC Rule 805) and the holder has until
5:30 p.m. ET to decide (FINRA Rule 2360). If the stock closes right at the strike, the seller does not know until the
next day whether they will be assigned: it depends on what the price does after the close and on what the holders
decide. That is **pin risk**.

```python
filas = []
for v in sorted(op_a["vencimiento"].unique()):
    if v > sesiones[-1]:
        continue
    ultimo = op_a.loc[op_a["vencimiento"] == v, "fecha"].max()
    ks = op_a[(op_a["fecha"] == ultimo) & (op_a["vencimiento"] == v)
              & (op_a["tipo"] == "C")].set_index("strike")
    k = ks.index[np.abs(ks.index - krn[v]).argmin()]
    filas.append({"vencimiento": v.date(), "cierre": krn[v],
                  "strike": k, "distancia": krn[v] - k,
                  "interes_abierto": ks.at[k, "interes_abierto"]})
pin = pd.DataFrame(filas)
cerca = pin["distancia"].abs() <= 0.25
print(f"{cerca.sum()} of {len(pin)} expirations within 0.25 USD "
      f"of a strike (at random: {len(pin) * 0.5 / 2.5:.1f})")
pin[cerca].tail(4)
```
```output
11 of 48 expirations within 0.25 USD of a strike (at random: 9.6)
   vencimiento  cierre  strike  distancia  interes_abierto
28  2024-05-17  137.35   137.5      -0.15             5368
30  2024-07-19  159.84   160.0      -0.16             4967
37  2025-02-21  192.32   192.5      -0.18             6418
46  2025-11-21  172.67   172.5       0.17             4853
```

In the data, 11 of the 48 KRN expirations close within 0.25 USD of a strike, versus 9.6 that chance would give with
strikes every 2.5: in the data there is no attraction toward the strikes, only arithmetic. In real markets the
literature documents that the closes of stocks with listed options cluster near the strikes on expiration days (Ni,
Pearson and Poteshman, 2005, in the resources). One of the cases in the data, 2024-07-19:

```python
rv = np.log(krn).diff().std()  # daily standard deviation
S_v, K_v, n = 159.84, 160.0, 10
print(f"KRN daily volatility: {rv:.2%}")
print(f"If there is no assignment: 0 shares. If there is: "
      f"{-100 * n} shares at {K_v}")
print(f"One-day risk of 1000 shares: "
      f"{1000 * S_v * rv:,.0f} USD (1 standard deviation)")
```
```output
KRN daily volatility: 1.69%
If there is no assignment: 0 shares. If there is: -1000 shares at 160.0
One-day risk of 1000 shares: 2,708 USD (1 standard deviation)
```

KRN closes at 159.84 with 4,967 open contracts at the 160 strike. With 10 short 160 calls, on Monday the position may
be zero shares or −1,000. If there is news after the close and the stock rises, the holders will exercise. One day of
1,000 KRN shares is a risk of about 2,700 USD per standard deviation; an earnings release or weekend news, several
times that. Short puts have the same problem in reverse.

The delta explains why it cannot be hedged in advance:

```python
Sg = np.array([155.0, 158.0, 159.0, 159.5, 160.0, 160.5, 161.0,
               162.0, 165.0])
perfil = {f"{d} days": delta_bsm("C", Sg, 160.0, d / 365, 0.25,
                                  0.04, 0.0)
          for d in (10, 2, 0.25)}
pd.DataFrame(perfil, index=Sg).round(3)
```
```output
       10 days  2 days  0.25 days
155.0    0.236   0.045      0.000
158.0    0.399   0.255      0.028
159.0    0.458   0.375      0.171
159.5    0.489   0.441      0.319
160.0    0.519   0.508      0.503
160.5    0.549   0.575      0.686
161.0    0.578   0.640      0.831
162.0    0.636   0.756      0.972
165.0    0.785   0.954      1.000
```

*Fig. — Black-Scholes delta of a 160-strike call (25% volatility, 4% rate) 10 days, 2 days and 6 hours before expiry: near expiry, delta jumps from 0 to 1 within a few dollars.* Three delta curves against the KRN price, from 150 to 170 USD. At 10 days the curve is smooth: 0.24 at 155, 0.52 at 160 and 0.79 at 165. At 2 days it is steeper: 0.05 at 155 and 0.95 at 165. At 6 hours (highlighted) it is almost a step: 0.03 at 158, 0.50 at 160 and 0.97 at 162.

At 10 days, the delta of the 160 call goes from 0.24 to 0.79 between 155 and 165. Six hours before expiration it is
almost a step: 0.03 at 158, 0.50 at 160 and 0.97 at 162. Hedging the delta of an option pinned to the strike in the
last session forces you to buy and sell almost the whole notional with each one-dollar move (lesson 3.6.2). The
practical solution is to close the short positions near the strike before the close on expiration day, accepting the
bid-ask spread.

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

The calendar of the course data has a peculiarity: the KRN chain does not include the series that expires on the
expiration day itself; its last row is the previous session. The cells use the close of expiration day for the
underlying and the strikes listed on the eve.

</aside>

## Cash-settled indices

Options on indices such as SPX are European and cash settled: there is no early assignment or delivery of shares,
and the two previous risks disappear. Another one remains: the settlement price. In standard SPX options, according
to the Cboe specifications, trading ends the day before (normally a Thursday) and the settlement value (SET) is
computed with the opening prices of the components on expiration day: a position held until the end is exposed to
Friday's open without being able to close. In the ORX data it settles at the close of expiration day, for
simplicity. Before holding a position until expiration, read in the contract specifications how and when the
settlement price is set.

## Exercises

1. **Another delta.** Repeat the rules table and the separation of the three worst trades with −0.30 delta puts.
   Does the conclusion change?

**Solution.**

With puts closer to the money the credit is larger and the conclusion holds. In total, the stop adds up to 90.05 and
holding to 68.41; without the three worst trades, holding adds up to 113.62 and the stop to 105.94. The time rules
stay at 48-49 with or without those episodes: they cut the tail and give up a good part of the theta. Holding's worst
trade worsens to −26.01% and the stop's to −10.02%: with more delta, the gap the stop absorbs is larger.

```python
ops30 = ciclo(0.30)
pnl30, tabla30 = resultados(ops30)
peores30 = pnl30["hold"].nsmallest(3).index
print(tabla30.round(2))
pd.DataFrame({"total": pnl30.sum(),
              "without the 3 worst": pnl30.drop(peores30).sum()}).round(2)
```
```output
                media  mediana  ganadoras   peor   dias  total
hold             0.48     0.98       0.89 -26.01  30.55  68.41
50%              0.27     0.59       0.93 -26.01  13.58  39.12
21 days          0.35     0.39       0.83  -4.73   9.66  49.47
stop 2x          0.63     0.96       0.86 -10.02  29.65  90.05
50% or 21 days   0.32     0.39       0.83  -4.73   8.32  45.27
                total  without the 3 worst
hold            68.41               113.62
50%             39.12                62.82
21 days         49.47                48.26
stop 2x         90.05               105.94
50% or 21 days  45.27                44.07
```

2. **Rolling without lowering the strike.** In the 2018 example, instead of selling the March −0.16 delta put, sell
   the March 2525 put (same strike, more time). Compute the roll's credit and the result of the new trade.

**Solution.**

Selling the same strike one month further out collects 32.6 points more than the buyback costs (a roll for a credit),
but the new put is in the money: it is a much bigger bet than the −0.16 delta one. ORX expires in March at 2315.19
and the new trade loses 70.71 points. Combined, the two trades lose 167.3 (−96.6 from the first and −70.7 from the
second), almost as much as holding the original (−194.87). The roll's credit was not a gain: it was the premium of a
new risk.

```python
fila = por_dia[tocada]
fila = fila[(fila["vencimiento"] == v2)
            & (fila["strike"] == o["strike"])].iloc[0]
pago = max(o["strike"] - S[v2], 0.0)
print(f"Buyback {cierre} · sale of the March 2525 at "
      f"{fila['bid']} · roll credit {fila['bid'] - cierre:.1f}")
print(f"ORX on {v2.date()}: {S[v2]} · result of the new one: "
      f"{fila['bid'] - pago:.2f}")
```
```output
Buyback 106.5 · sale of the March 2525 at 139.1 · roll credit 32.6
ORX on 2018-03-16: 2315.19 · result of the new one: -70.71
```

3. **Pin risk.** You are short 10 KRN 160 calls and the stock trades at 159.90 one hour before the close on expiration
   day. List your options and the risk each one leaves.

**Solution.**

Buy back the calls: you pay the bid-ask spread and eliminate the risk. Do nothing: if the stock closes below 160 and
there is no news, they expire worthless; if it rises after the close, you may be assigned and on Monday you will be
short 1,000 shares (about 2,700 USD of risk per one-day standard deviation, more if there is news). Buy 500 shares as
a partial hedge: it leaves a position of +500 or −500 shares depending on whether there is assignment or not, the same
uncertainty with the other sign. Roll to the next expiration: it closes today's risk and opens a new position, which
is evaluated as such. The most common choice on a desk is to buy back the short positions less than one strike away
from the price before the close.

## Self-assessment

1. In the monthly sale of −0.16 delta puts, the rule “close at 50% of the credit” wins more often (98% winning trades) but adds up to less than holding (23.8 versus 41.2% of the strike). Why?

   - A. Because it gives up the theta of the remaining days in normal trades and does not protect against drops that arrive before the target is reached: its worst trade is the same, −25.0%
   - B. Because it pays more commissions
   - C. Because 50% is too ambitious a target
   - D. Because closing at the ask is free

   *Answer A: Because it gives up the theta of the remaining days in normal trades and does not protect against drops that arrive before the target is reached: its worst trade is the same, −25.0%.* A profit target only acts when the trade goes well. The February 2020 drop arrived before the put lost half of its value, so the rule suffers it in full. Over the 140 normal trades it adds up to 48.3 versus 78.3 for holding.

2. The stop at twice the credit adds up to 65.8 versus 41.2 for holding. Which conclusion is correct?

   - A. That the stop is a proven edge in premium selling
   - B. That holding has a calculation error
   - C. That the stop eliminates tail risk
   - D. That three trades decide the difference: without them, holding adds up to 78.3 and the stop to 73.0; with 143 cycles, the comparison depends on how three episodes unfolded

   *Answer D: That three trades decide the difference: without them, holding adds up to 78.3 and the stop to 73.0; with 143 cycles, the comparison depends on how three episodes unfolded.* Over the 140 normal trades the stop triggers now and then and loses what holding would have recovered. Its total advantage comes from 2018 and 2020, when the drop unfolded over several sessions. In an opening gap, a stop fills much further away: in the data, up to 6.9 times the credit instead of 2.

3. A short put is bought back at 106.5 and at the same moment another one with a later expiration is sold at 30.2. What is the result of the original trade?

   - A. −66.4 points, because the roll is done for a credit
   - B. Zero, because it has been rolled
   - C. The initial credit minus 106.5 (−96.6 points): the roll is a close and an open; the 30.2 is the premium of a new trade, with its own risk
   - D. It is not known until the new expiration

   *Answer C: The initial credit minus 106.5 (−96.6 points): the roll is a close and an open; the 30.2 is the premium of a new trade, with its own risk.* Rolling does not repair a loss: it realizes it and opens another position. In the 2018 example, the new put expired worthless and added 30.2 points, but it might not have; merging the two trades hides the loss of the first one.

4. On the eve of the ex-date, a deep in-the-money American call on KRN is worth exactly its intrinsic value and the equivalent European one 0.86 USD less. What does a rational holder do and what happens to the seller?

   - A. Nothing: options have no dividends
   - B. Exercises to collect the 0.88 USD dividend; the seller is assigned, delivers the shares and loses that dividend
   - C. Sells the stock
   - D. Waits until expiration, because exercising early is never optimal

   *Answer B: Exercises to collect the 0.88 USD dividend; the seller is assigned, delivers the shares and loses that dividend.* Exercising before the ex-date gives the right to the dividend; waiting keeps a time value that here is almost zero. For the seller, the assignment arrives before the ex-date and they have to deliver the shares (or end up short of them and owe the dividend).

5. KRN closes at 159.84 USD on expiration day and you are short 10 160 calls. What is the risk?

   - A. None: the calls expire out of the money
   - B. Not knowing until after the close whether you will be assigned: the holder can decide to exercise until 5:30 p.m. ET; on Monday you may wake up short 1,000 shares, with a one-day risk of about 2,700 USD per standard deviation
   - C. That the broker charges a commission
   - D. That the calls are worth more than 160

   *Answer B: Not knowing until after the close whether you will be assigned: the holder can decide to exercise until 5:30 p.m. ET; on Monday you may wake up short 1,000 shares, with a one-day risk of about 2,700 USD per standard deviation.* Options out of the money by cents are not exercised automatically, but the holder can submit a contrary instruction until the 5:30 p.m. ET cutoff (FINRA Rule 2360). With news after the close, they will. The seller will not know until the next day.

## Key points

- Management rules change the shape of the result, they do not create an edge: closing at 50% raises the hit rate and lowers the total; closing on time or on loss cuts the tail at the expense of normal trades.
- In premium selling, very few trades decide the comparison between rules (here, 3 out of 143): before choosing a rule, look at where the difference comes from.
- A stop on options is checked at the close and filled at the ask: in gaps it fills much further away from where it was placed.
- Rolling is closing and opening. The loss of the first trade is realized; the second is a new bet.
- American options on stocks can be exercised before expiration: deep in-the-money calls before the ex-date and deep in-the-money puts with positive rates. The seller must watch for it.
- A close pinned to the strike of a physically settled option leaves Monday's position up in the air: close it before expiration if you do not accept the uncertainty.

## Resources

### Documentation

- [FINRA: Rule 2360 (exercise by exception, 5:30 p.m. ET cutoff and random or FIFO assignment)](https://www.finra.org/rules-guidance/rulebooks/finra-rules/2360) · finra.org
- [Cboe: S&P 500 option specifications (European style, SET settlement)](https://www.cboe.com/tradable_products/sp_500/spx_options/specifications/) · cboe.com
- [Ni, Pearson and Poteshman (2005), Stock price clustering on option expiration dates, Journal of Financial Economics 78](https://doi.org/10.1016/j.jfineco.2004.08.005) · doi.org

### Books

- [John C. Hull, Options, Futures, and Other Derivatives, 11th ed. (Pearson, 2021)](https://www.pearson.com/store/en-us/p/options-futures-and-other-derivatives/P200000005938) · pearson.com

### Data

- [Level 3 data: KRN American option chain (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/accion3_opciones.parquet) · Download · accion3_opciones.parquet
- [Level 3 data: ORX daily option chain (Parquet)](https://pythonparatrading.com/curso/datos/nivel-3/opciones3_cadena.parquet) · Download · opciones3_cadena.parquet

Previous 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/)

Next lesson: [3.6.1 · Systematic premium selling: put-write, strangles and their tail risk](https://pythonparatrading.com/en/python-trading-course/level-3/systematic-premium-selling-tail-risk/)
