Lesson 3.5.5 · 65 min

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.

Fig. — Monthly sale of a −0.16 delta put on ORX, 2014-2025, under five management rules: sum of the P&L of the 140 normal trades and of the 3 worst under the hold rule (2018-01, 2018-09 and 2020-02)
Level 3 lessons
  1. 3.1 · Futures in depth

    1. 3.1.1 Main futures contracts
    2. 3.1.2 Futures curve: basis, cost of carry, contango and roll yield
    3. 3.1.3 Roll management, margins, SPAN and implied leverage
    4. 3.1.4 Diversified futures systems: Carver's approach
  2. 3.2 · Futures spreads

    1. 3.2.1 Calendar spreads, butterflies and condors
    2. 3.2.2 Intercommodity spreads: crack, spark and crush
    3. 3.2.3 Rate and index spreads: 2s10s, NOB, NQ/ES and Europe versus the U.S.
    4. 3.2.4 Hedge ratios: by notional, by DV01 and by volatility
    5. 3.2.5 Spread data and backtesting: listed, synthetic and legged
    6. 3.2.6 Seasonality of spreads: multi-year analysis, patterns and traps
  3. 3.3 · Advanced forex

    1. 3.3.1 FX microstructure: OTC, ECN, last look and sessions
    2. 3.3.2 Carry and crash risk, momentum and value in currencies; baskets
    3. 3.3.3 Currency risk in multi-currency portfolios; execution via OANDA, IBKR or MT5
  4. 3.4 · Option theory and pricing

    1. 3.4.1 Payoffs, put-call parity and arbitrage bounds
    2. 3.4.2 Binomial trees, Black-Scholes-Merton, Black-76 and Monte Carlo
    3. 3.4.3 Greeks: delta, gamma, vega, theta, rho, vanna, volga and charm
    4. 3.4.4 Implied volatility: Newton-Raphson, Brent, py_vollib and QuantLib
    5. 3.4.5 Smile, skew and term structure; volatility surface with SVI
    6. 3.4.6 Realized vs implied volatility; volatility risk premium; VIX and VSTOXX
    7. 3.4.7 Advanced models: Heston, SABR and local volatility
  5. 3.5 · Option structures in Python

    1. 3.5.1 Directional and vertical structures: call, put, covered call, collar and spreads
    2. 3.5.2 Volatility and range structures: straddles, strangles, condors and butterflies
    3. 3.5.3 Calendars, diagonals, ratios, backspreads, risk reversals and jade lizard
    4. 3.5.4 A custom tool: payoff, aggregate Greeks and scenarios
    5. 3.5.5 Management: adjustments, rolls, early closes, early assignment and pin risk
  6. 3.6 · Systematic volatility and options strategies

    1. 3.6.1 Systematic premium selling: put-write, strangles and their tail risk
    2. 3.6.2 Delta hedging, gamma scalping and dispersion
    3. 3.6.3 VIX and VSTOXX strategies: volatility futures and ETPs
    4. 3.6.4 Options backtesting: data, fills and event adjustments
  7. 3.7 · Crypto derivatives and specific strategies

    1. 3.7.1 Perpetuals: funding mechanics, mark price and liquidations
    2. 3.7.2 Funding arbitrage, basis with futures and cross-exchange arbitrage
    3. 3.7.3 Crypto options, on-chain data and DeFi
    4. 3.7.4 ccxt for trading on multiple exchanges
    5. 3.7.5 MiCA: the EU framework for crypto-assets and their service providers (CASPs)

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:

adjustmentcloserollnew positionexpirationexerciseOpenAdjustedClosedRolledExpiredAssigned
Mermaid source
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 --> [*]
DecisionWhat it isWhat changes in the GreeksCost
Let it expireDo nothing until expirationNothing; gamma grows as expiration nearsNone, except settlement or delivery
Close earlyBuy back what was sold (or sell what was bought)Everything to zeroExit bid-ask spread
RollClose and open another position further out in time or in strikeThose of the new positionTwo bid-ask spreads
AdjustAdd or remove legs (wings, hedges with the underlying, change a strike)The ones you want to correctOne or more bid-ask spreads
Assignment or exerciseThe counterparty exercises (American options) or expiration arrives in the moneyOption replaced by underlying or cashLost 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.

IN [01]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")
OUT [01]ORX index and puts
3017 days with a chain · 147 expirations
IN [02]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")
OUT [02]One −0.16 delta put per monthly cycle
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.

IN [03]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)
OUT [03]Five management rules
                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:

IN [04]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)
OUT [04]Which trades decide the comparison?
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.

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.

IN [05]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)
OUT [05]The stop does not fill where it is placed
      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:

IN [06]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)
OUT [06]Rolling a tested put
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.

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:

AdjustmentExampleMain effect
Buy a wingTurn a short put into a put spreadMaximum loss capped; less theta and less short vega
Hedge the deltaSell or buy the underlying or futuresDelta to zero; gamma and vega unchanged
Roll the untested legIn a strangle after a drop, raise the short callRecenters the delta collecting a bit more premium; increases risk if the market rebounds
Reduce sizeBuy back part of the contractsAll 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:

IN [07]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)
OUT [07]KRN: American options with dividends
     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 DD; if not, the stock drops by DD 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(1−e−rτ)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:

IN [08]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()
OUT [08]Calls on the eve of the ex-date
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:

IN [09]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)
OUT [09]Time value of a call before the ex-date
                 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)

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

noyesnoyesShort ITM callEx-date beforeLow riskTV less than DExpect assignmentClose or roll
Mermaid source
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:

IN [10]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)
OUT [10]In-the-money puts: early exercise
            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.

IN [11]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)
OUT [11]How many expirations close pinned to a strike?
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:

IN [12]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)")
OUT [12]Ten short calls closing at the strike
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:

IN [13]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)
OUT [13]Delta of a call near expiration
       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

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.

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.

IN [14]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)
OUT [14]Rules with −0.30 delta puts
                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
  1. 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.

IN [15]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}")
OUT [15]Roll to the same strike
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
  1. 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.

Each answer is folded under its question

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?

    • ABecause 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%
    • BBecause it pays more commissions
    • CBecause 50% is too ambitious a target
    • DBecause closing at the ask is free
    Show answer

    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?

    • AThat the stop is a proven edge in premium selling
    • BThat holding has a calculation error
    • CThat the stop eliminates tail risk
    • DThat 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
    Show answer

    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
    • BZero, because it has been rolled
    • CThe 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
    • DIt is not known until the new expiration
    Show answer

    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?

    • ANothing: options have no dividends
    • BExercises to collect the 0.88 USD dividend; the seller is assigned, delivers the shares and loses that dividend
    • CSells the stock
    • DWaits until expiration, because exercising early is never optimal
    Show answer

    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?

    • ANone: the calls expire out of the money
    • BNot 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
    • CThat the broker charges a commission
    • DThat the calls are worth more than 160
    Show answer

    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

Data