Lesson 1.2.5 · 60 min

Options: calls, puts, intrinsic value and time value

An option is a right with an expiry date, and its price splits into what exercising it is worth today and what waiting is worth. The lesson measures this on one day's full option chain of the fictitious FIX index: 584 contracts, four expiries.

Fig. — The FIX option chain expiring on 18 July 2025 as a strike ladder: bid and ask of each call and put at the close of 2 June. Shaded, the in-the-money options
05/07
Module
1.2 · Asset classes
Duration
60 min
Updated
Level 1 lessons
  1. 1.1 · How markets work

    1. 1.1.1 Market participants and trading venues
    2. 1.1.2 Sessions, auctions, holidays and calendars
    3. 1.1.3 Order book: bid, ask, spread and depth
    4. 1.1.4 Order types
    5. 1.1.5 Costs, settlement, leverage and margin
  2. 1.2 · Asset classes

    1. 1.2.1 Stocks: splits, dividends and corporate actions
    2. 1.2.2 ETFs: replication, tracking error, leveraged ETFs and UCITS
    3. 1.2.3 Futures: specifications, margin, roll and contango
    4. 1.2.4 Forex: pairs, pips, lots, rollover and CFDs
    5. 1.2.5 Options: calls, puts, intrinsic value and time value
    6. 1.2.6 Crypto: spot, perpetuals, funding and custody
    7. 1.2.7 Fixed income and interest rates; CFDs and leveraged products
  3. 1.3 · Market data sources

    1. 1.3.1 Types of market data: EOD, bars, ticks, order book and reference
    2. 1.3.2 Free market data sources
    3. 1.3.3 Retail and prosumer market data APIs
    4. 1.3.4 Professional market data providers
    5. 1.3.5 Crypto and on-chain data
    6. 1.3.6 Brokers as market data sources
    7. 1.3.7 Market data licensing and terms of use
    8. 1.3.8 How to choose a market data provider
  4. 1.4 · Financial data engineering

    1. 1.4.1 Downloading over REST and websockets: pagination, rate limits and retries
    2. 1.4.2 Data cleaning: missing values, bad ticks, gaps and time zones
    3. 1.4.3 Adjusting for splits and dividends: adjusted and unadjusted prices
    4. 1.4.4 Universes without survivorship bias: delistings, mergers and symbol changes
    5. 1.4.5 Building bars: time, tick, volume, dollar and imbalance
    6. 1.4.6 Continuous futures series: roll rules, difference adjustment and ratio adjustment
    7. 1.4.7 Storage: CSV, Parquet, partitioning, DuckDB, TimescaleDB and ArcticDB
    8. 1.4.8 Reproducible pipeline: download, validation, incremental updates and quality tests
  5. 1.5 · Fundamental and macro data

    1. 1.5.1 Financial statements, ratios, XBRL and SEC EDGAR
    2. 1.5.2 Macro data, economic calendars, estimates and transcripts
  6. 1.6 · Financial exploratory analysis

    1. 1.6.1 Returns, volatility, rolling correlations and drawdown
    2. 1.6.2 Stylized facts: fat tails, clustering, autocorrelation and leverage
    3. 1.6.3 Seasonality: day of the week, month, turn of the month and intraday sessions
  7. 1.7 · Programmed technical analysis

    1. 1.7.1 Technical indicators in Python
    2. 1.7.2 Your own implementation versus TA-Lib: validating results
    3. 1.7.3 Candlestick patterns and support and resistance
    4. 1.7.4 Multi-timeframe analysis without look-ahead

A right with an expiry date

An option is a contract that gives its buyer the right, but not the obligation, to buy or sell an asset (the underlying) at a price fixed in advance (the strike or exercise price) until a date (the expiry). There are two types:

  • A call gives the right to buy the underlying at the strike. It suits someone who expects it to rise.
  • A put gives the right to sell the underlying at the strike. It suits someone who expects it to fall or wants protection against a drop.

For that right, the buyer pays a premium to the seller at the time of purchase. The seller (also called the writer) receives the premium and takes on the opposite obligation: if the buyer exercises, the seller has to sell (for the call) or buy (for the put) at the strike. The asymmetry is the key to everything that follows: the buyer risks at most the premium; the seller collects a small, certain amount in exchange for a risk that can be large.

premiumyesnoBuyerSellerEnds in the money?ExercisesExpires worthlessSeller pays
Mermaid source
flowchart TB
  C[Buyer] -- premium --> V[Seller]
  C --> X{Ends in the money?}
  X -- yes --> E[Exercises]
  X -- no --> N[Expires worthless]
  E --> L[Seller pays]

At expiry, the buyer exercises only if it pays to do so: a call if the underlying is above the strike, a put if it is below. Settlement can be physical (the shares or the underlying future are delivered) or cash (the difference between the settlement price and the strike is paid). Index options, like the ones in this lesson, are cash-settled: an index cannot be delivered.

Each contract has a multiplier: how many units of money one point of premium is worth. In this lesson's options it is 100 USD per point, as in Cboe's SPX options on the S&P 5001Note 1 · SPX option specifications on the Cboe page linked in the resources: 100 USD multiplier, European style and cash settlement.: a premium of 174.05 points costs 17,405 USD per contract. Forgetting the multiplier is the cheapest mistake to make in a notebook and the most expensive in a real account.

The FIX chain

The data are a snapshot of the option chain of a fictitious index, FIX, at the close of June 2, 2025: all the listed calls and puts, for four expiries and dozens of strikes, with their best bid and ask, the day's volume and the open interest (outstanding contracts). They are European options with cash settlement. The index closed that day at 6,359.10 (fix_indice_diario.csv, the same index as the futures in lesson 1.2.3).

IN [01]python
import numpy as np
import pandas as pd

cad = pd.read_csv("opciones_cadena_fix.csv",
                  parse_dates=["fecha", "vencimiento"])
indice = pd.read_csv("fix_indice_diario.csv", parse_dates=["fecha"])
S = indice.set_index("fecha").loc["2025-06-02", "indice"]
print(f"{len(cad)} options; FIX at the close: {S}")
cad.groupby(["vencimiento", "tipo"]).agg(
    strikes=("strike", "size"), desde=("strike", "min"),
    hasta=("strike", "max"), interes=("interes_abierto", "sum"))
OUT [01]The FIX option chain
584 options; FIX at the close: 6359.1
                  strikes  desde  hasta  interes
vencimiento tipo
2025-06-20  call       50   5675   6900   412880
            put        50   5675   6900   621931
2025-07-18  call       82   5275   7300   574545
            put        82   5275   7300   863427
2025-09-19  call       66   4700   7950   414879
            put        66   4700   7950   622741
2025-12-19  call       94   4150   8800   497606
            put        94   4150   8800   746209

Four expiries, the third Friday of June, July, September and December, and in each one the same number of calls and puts: every strike has its pair. The longer the expiry, the wider the range of strikes, because the index can move further away. The open interest of the puts is larger than that of the calls: on a stock index, puts are the insurance of portfolios, and there is more demand for insurance than for bets on a rise.

A listing like that is hard to read. The usual way to look at a chain is the one broker platforms use: one row per strike, calls on the left and puts on the right.

IN [02]python
cad["medio"] = (cad["bid"] + cad["ask"]) / 2
jul = cad[cad["vencimiento"] == "2025-07-18"]
tabla = jul.pivot(index="strike", columns="tipo",
                  values=["bid", "ask"])
tabla.columns = [f"{t}_{c}" for c, t in tabla.columns]
tabla = tabla[["call_bid", "call_ask", "put_bid", "put_ask"]]
tabla.loc[6200:6500:2]
OUT [02]July expiry, in chain format
        call_bid  call_ask  put_bid  put_ask
strike
6200       274.0     275.2     92.1     92.6
6250       238.4     239.5    106.1    106.7
6300       204.9     205.8    122.3    122.9
6350       173.6     174.5    140.7    141.4
6400       144.9     145.6    161.5    162.3
6450       118.8     119.4    185.1    185.9
6500        95.5      96.0    211.4    212.4

The figure at the top draws the same chain as a strike ladder: each rung is a strike, with the bid and ask of its call on the left and those of its put on the right, and the amber line marks where the index is. The shaded areas are the in-the-money options: calls with a strike below the index (top left) and puts with a strike above it (bottom right). Going down the ladder, toward higher strikes, calls get cheaper and puts get more expensive. Each 50-point rung changes the call's mid price by less than 50 points: about 41 between 6,050 and 6,100, about 29 between 6,350 and 6,400 and about 15 between 6,600 and 6,650. That sensitivity of the premium to the strike is related to the probability of ending in the money (level 3).

What each position pays at expiry

At expiry, an option no longer has any uncertainty: it is worth what exercising it yields. With STS_T the underlying at expiry and KK the strike:

call: max⁡(ST−K, 0)put: max⁡(K−ST, 0)\text{call: } \max(S_T - K,\ 0) \qquad \text{put: } \max(K - S_T,\ 0)

That is the payoff. The position's result (P&L) subtracts the premium paid, or adds the premium received if the option was sold. The four combinations —call or put, bought or sold— give the four basic positions, and every option strategy (spreads, straddles, condors) is a sum of them. They are computed with the July at-the-money options, strike 6,350, bought or sold at the mid price:

IN [03]python
def pnl_vencimiento(s_t, strike, prima, tipo, lado=1):
    """P&L per unit of underlying; lado 1 buy, -1 sell."""
    s_t = np.asarray(s_t, dtype=float)
    if tipo == "call":
        payoff = np.maximum(s_t - strike, 0)
    else:
        payoff = np.maximum(strike - s_t, 0)
    return lado * (payoff - prima)

K = 6350
prima = jul[jul["strike"] == K].set_index("tipo")["medio"]
print(prima.to_dict())
escenarios = [6000, 6200, 6350, 6500, 6700]
pd.DataFrame({
    "long call": pnl_vencimiento(escenarios, K,
                                 prima["call"], "call"),
    "short call": pnl_vencimiento(escenarios, K,
                                  prima["call"], "call", -1),
    "long put": pnl_vencimiento(escenarios, K,
                                prima["put"], "put"),
    "short put": pnl_vencimiento(escenarios, K,
                                 prima["put"], "put", -1),
}, index=pd.Index(escenarios, name="FIX at expiry"))
OUT [03]P&L at expiry of the four basic positions
{'call': 174.05, 'put': 141.05}
               long call  short call  long put  short put
FIX at expiry
6000             -174.05      174.05    208.95    -208.95
6200             -174.05      174.05      8.95      -8.95
6350             -174.05      174.05   -141.05     141.05
6500              -24.05       24.05   -141.05     141.05
6700              175.95     -175.95   -141.05     141.05
Fig. — Expiry P&L of the four basic positions with the July 6,350-strike options, at the 2 June 2025 mid price (solid line, long; dashed, short)

Each column is the mirror image of its neighbor: what the buyer gains the seller loses, point for point. It is worth recognizing the four shapes at a glance:

PositionViewMaximum gainMaximum lossBreakeven
Long callBullishUnlimitedThe premiumK+premiumK + \text{premium}
Short callNot bullishThe premiumUnlimitedK+premiumK + \text{premium}
Long putBearishK−premiumK - \text{premium}The premiumK−premiumK - \text{premium}
Short putNot bearishThe premiumK−premiumK - \text{premium}K−premiumK - \text{premium}
IN [04]python
mult = 100  # USD per point of premium
print(f"Call breakeven: {K + prima['call']:.2f}")
print(f"Put breakeven:  {K - prima['put']:.2f}")
print(f"One call costs {prima['call'] * mult:,.0f} USD")
subida = (K + prima["call"]) / S - 1
print(f"The call makes money if FIX rises more than {subida:.2%}")
OUT [04]Breakevens and size in dollars
Call breakeven: 6524.05
Put breakeven:  6208.95
One call costs 17,405 USD
The call makes money if FIX rises more than 2.59%

Buying the at-the-money call only makes a profit if the index rises more than 2.59% in the six weeks left until expiry. That is the other side of the limited loss: the premium has to be recovered before anything is gained. The call seller makes money as long as the index does not go above 6,524.05: in most scenarios it gains a little, at most the premium; in a few it loses much more than it could have gained.

Intrinsic value and extrinsic value

Before expiry, an option's premium is greater than its immediate payoff. It splits into two parts:

  • Intrinsic value: what exercising it now would yield, max⁡(S−K,0)\max(S - K, 0) for a call and max⁡(K−S,0)\max(K - S, 0) for a put. It is never negative.
  • Extrinsic value or time value: the rest, premium minus intrinsic value. It is what is paid for the possibility that the underlying moves favorably before expiry.

The relation between strike and underlying is called moneyness and gives three categories:

CategoryCallPutIntrinsic value
In the money (ITM)K<SK < SK>SK > SPositive
At the money (ATM)K≈SK \approx SK≈SK \approx SAlmost zero
Out of the money (OTM)K>SK > SK<SK < SZero
IN [05]python
def moneyness(tipo, strike, s, banda=0.005):
    """ITM, ATM (within 0.5% of the index) or OTM."""
    dist = (s - strike) / s if tipo == "call" else (strike - s) / s
    if abs(dist) < banda:
        return "ATM"
    return "ITM" if dist > 0 else "OTM"

es_call = cad["tipo"] == "call"
cad["intrinseco"] = np.where(es_call, S - cad["strike"],
                             cad["strike"] - S).clip(min=0)
cad["extrinseco"] = cad["medio"] - cad["intrinseco"]
cad["moneyness"] = [moneyness(t, k, S) for t, k in
                    zip(cad["tipo"], cad["strike"])]
cols = ["tipo", "strike", "medio", "intrinseco", "extrinseco",
        "moneyness"]
jul = cad[cad["vencimiento"] == "2025-07-18"]
jul[jul["strike"].isin([6000, 6350, 6700])][cols].round(2)
OUT [05]Intrinsic, extrinsic and moneyness
     tipo  strike   medio  intrinseco  extrinseco moneyness
158  call    6000  433.75       359.1       74.65       ITM
159   put    6000   52.65         0.0       52.65       OTM
186  call    6350  174.05         9.1      164.95       ATM
187   put    6350  141.05         0.0      141.05       ATM
214  call    6700   31.15         0.0       31.15       OTM
215   put    6700  346.20       340.9        5.30       ITM

The six rows sum up the idea. The 6,000 call is in the money: of its premium of 433.75 points, 359.10 are intrinsic value and 74.65, time value. The 6,700 call is out of the money: all its premium is time value. The two at the 6,350 strike are at the money, with an intrinsic value of 9.10 points for the call and zero for the put, and they are the ones with the most time value of the six.

Fig. — Premium of the July FIX calls against their intrinsic value, by strike, at the close of 2 June 2025. The gap between them is the extrinsic value

The figure extends this to all the July calls. The gray area is the intrinsic value, the straight line that starts at the strike equal to the index; the line, the premium. What is left between the two is the extrinsic value, and it is bell-shaped, centered on the index. Deep in the money the call behaves almost like the index itself; deep out of the money, it is worth almost nothing; near the money, the final outcome is more uncertain, and the uncertainty is what is paid for.

IN [06]python
idx = cad.groupby(["vencimiento", "tipo"])["extrinseco"].idxmax()
maximos = cad.loc[idx].set_index(["vencimiento", "tipo"])
maximos[["strike", "extrinseco"]].round(2)
OUT [06]Where extrinsic value peaks
                  strike  extrinseco
vencimiento tipo
2025-06-20  call    6350       94.40
            put     6350       85.05
2025-07-18  call    6350      164.95
            put     6350      141.05
2025-09-19  call    6350      280.35
            put     6350      223.95
2025-12-19  call    6350      414.10
            put     6350      311.30

In all four expiries and for both types, the maximum falls at the 6,350 strike, the closest to the index. Calls and puts with the same strike do not have the same extrinsic value: in July, 164.95 points for the call and 141.05 for the put. The difference comes from the forward, which is above the index (see the parity section), but the bell shape is the same for both.

Time value decays faster and faster

Extrinsic value depends on time to expiry. More time means more room for the underlying to move, and therefore more value. But the relation is not linear: with a price model of independent random moves, the dispersion of the underlying at expiry grows with the square root of time (lesson 0.7.1), and so, approximately, does the time value of an at-the-money option.

To measure it without the distortion of the forward, take the average of the extrinsic value of the call and the put at the 6,350 strike: the call has more extrinsic value than the put because the forward is above the index (the call is already in the money relative to the forward), and averaging the two offsets that effect.

IN [07]python
del_dinero = cad[cad["strike"] == 6350]
atm = del_dinero.pivot(index="vencimiento", columns="tipo",
                       values="extrinseco")
atm["medio"] = (atm["call"] + atm["put"]) / 2
atm["dias"] = (atm.index - pd.Timestamp("2025-06-02")).days
atm["por_raiz_dia"] = atm["medio"] / np.sqrt(atm["dias"])
atm.round(2)
OUT [07]At-the-money time value by expiry
tipo           call     put   medio  dias  por_raiz_dia
vencimiento
2025-06-20    94.40   85.05   89.72    18         21.15
2025-07-18   164.95  141.05  153.00    46         22.56
2025-09-19   280.35  223.95  252.15   109         24.15
2025-12-19   414.10  311.30  362.70   200         25.65

If time value were proportional to time to expiry, the last column would be multiplied by 3.3 from the first expiry to the last (200/18\sqrt{200/18}); instead, it goes from 21 to 26 points per square-root day. That it rises somewhat with time to expiry is because this chain's implied volatility increases with expiry, something studied in level 3. With a single constant fitted to the four points, the curve V=adaysV = a\sqrt{\text{days}} shows how the time value of a 200-day at-the-money option is used up if nothing else changes:

IN [08]python
a = (atm["medio"] * np.sqrt(atm["dias"])).sum() / atm["dias"].sum()
print(f"a = {a:.2f} points per square-root day")
inicial = a * np.sqrt(200)
for desde, hasta in [(200, 170), (90, 60), (60, 30), (30, 0)]:
    perdida = a * (np.sqrt(desde) - np.sqrt(hasta))
    print(f"from {desde:>3} to {hasta:>3} days: loses {perdida:6.1f}"
          f" points ({perdida / inicial:.0%} of the initial value)")
OUT [08]How time value is used up
a = 24.61 points per square-root day
from 200 to 170 days: loses   27.2 points (8% of the initial value)
from  90 to  60 days: loses   42.8 points (12% of the initial value)
from  60 to  30 days: loses   55.8 points (16% of the initial value)
from  30 to   0 days: loses  134.8 points (39% of the initial value)
Fig. — Time value of the FIX 6,350-strike options (average of call and put) in the four expiries of the 2 June 2025 chain, with the a·√days curve fitted to the four points

With the curve fitted to the chain, a 200-day at-the-money option loses 27 points in its first month, 8% of its time value; in its last month it loses the 135 points it has left, 39% of the initial value. That daily erosion of time value has a name, theta, and it is one of the “Greeks”, the sensitivities of the premium to the underlying, volatility, time and the interest rate. The Greeks and implied volatility are covered in level 3; here the intuition is enough: the buyer of an option pays for the passage of time and the seller collects it.

Put-call parity

A long call and a short put with the same strike and expiry pay, at expiry, ST−KS_T - K in every scenario: the same as a future bought at KK. If two portfolios pay the same in the future, they have to cost the same today, and that gives the put-call parity of European options:

C−P=e−rT (F−K),F=S e(r−q)TC - P = e^{-rT}\,(F - K), \qquad F = S\,e^{(r - q)T}

with rr the interest rate, qq the index dividend yield (both as continuous rates) and FF the forward, the theoretical price of a future with the same expiry (lesson 1.2.3). For the date of the data, rr = 4.354% and qq = 1.350% (data README).

On a real chain it has to be checked with executable prices. Buying the call and selling the put costs Cask−PbidC_{ask} - P_{bid}; selling the call and buying the put brings in Cbid−PaskC_{bid} - P_{ask}. If the theoretical value falls between the two, no arbitrage is possible after paying the spreads:

IN [09]python
r, q = 0.04354, 0.01350
ancho = cad.pivot_table(index=["vencimiento", "strike"],
                        columns="tipo", values=["bid", "ask"])
ancho.columns = [f"{t}_{c}" for c, t in ancho.columns]
venc = ancho.index.get_level_values("vencimiento")
k = ancho.index.get_level_values("strike")
T = (venc - pd.Timestamp("2025-06-02")).days / 365
F = S * np.exp((r - q) * T)
ancho["teorico"] = np.exp(-r * T) * (F - k)
ancho["minimo"] = ancho["call_bid"] - ancho["put_ask"]
ancho["maximo"] = ancho["call_ask"] - ancho["put_bid"]
dentro = ancho["teorico"].between(ancho["minimo"], ancho["maximo"])
print(f"Strikes that satisfy parity: {dentro.sum()} of "
      f"{len(ancho)}")
ancho.loc[("2025-07-18", 6350),
          ["minimo", "teorico", "maximo"]].round(2)
OUT [09]Put-call parity within the spread
Strikes that satisfy parity: 292 of 292
minimo     32.20
teorico    33.04
maximo     33.80
Name: (2025-07-18 00:00:00, 6350), dtype: float64

All 292 pairs satisfy parity within the spread. At the July at-the-money strike, the theoretical value of C−PC - P is 33.04 points and the executable interval, from 32.20 to 33.80, is 1.6 wide. Parity also works the other way: from the mid prices of the call and the put you can infer the forward the market is pricing, without needing a future. It is a common way to estimate the implied dividend or to detect faulty data:

IN [10]python
ancho["medio_cp"] = ((ancho["call_bid"] + ancho["call_ask"])
                     - (ancho["put_bid"] + ancho["put_ask"])) / 2
ancho["F_implicito"] = k + ancho["medio_cp"] * np.exp(r * T)
resumen = ancho.groupby(level="vencimiento")["F_implicito"].median()
pd.DataFrame({"implied F": resumen,
              "theoretical F": S * np.exp((r - q) *
                  (resumen.index - pd.Timestamp("2025-06-02"))
                  .days.values / 365)}).round(2)
OUT [10]The forward implied by the chain
             implied F  theoretical F
vencimiento
2025-06-20     6368.52        6368.53
2025-07-18     6383.22        6383.22
2025-09-19     6416.42        6416.40
2025-12-19     6464.65        6464.64

The implied forward matches the theoretical one to within 0.02 points in all four expiries, and it is above the index because the interest rate exceeds the dividend yield: the same contango as the FIX futures.

American and European

The difference is when they can be exercised:

  • A European option is only exercised at expiry. European options include, for example, SPX options on the S&P 500, the ones in this lesson and many index options.
  • An American option can be exercised any day until expiry. As a general rule, options on individual stocks and on ETFs listed in the US are American, and when exercised they settle by delivering the shares2Note 2 · Cboe, on the SPX specifications page: index options (European) are only exercised at expiry; options on stocks and ETFs (American) can be exercised at any time before expiry. In its comparison, ETF options settle by delivery of the shares..

Being able to exercise early never subtracts value, so an American option is worth at least as much as the equivalent European one. Above all, an American option can never be worth less than its intrinsic value: if it cost less, you could simply buy it and exercise it on the spot. A European one can, and the FIX chain shows it:

IN [11]python
bajo = cad[(cad["tipo"] == "put") & (cad["extrinseco"] < 0)]
print(f"Puts with a premium below intrinsic value: {len(bajo)}")
print(bajo.groupby("vencimiento")["strike"].min())
peores = bajo.nsmallest(3, "extrinseco").set_index("vencimiento")
peores[["strike", "medio", "intrinseco", "extrinseco"]].round(2)
OUT [11]European puts below their intrinsic value
Puts with a premium below intrinsic value: 93
vencimiento
2025-06-20    6625
2025-07-18    6750
2025-09-19    6900
2025-12-19    7050
Name: strike, dtype: int64
             strike    medio  intrinseco  extrinseco
vencimiento
2025-12-19     8800  2280.35      2440.9     -160.55
2025-12-19     8750  2231.55      2390.9     -159.35
2025-12-19     8700  2182.75      2340.9     -158.15

The 93 affected puts are all deep in the money: from the 6,625 strike in June, 4% above the index, and from 7,050 in December, 11% above. Longer expiries need to be deeper in the money because more time value is left to offset. The reason for the phenomenon is the interest rate. A deep in-the-money put will almost certainly be exercised, and what it pays then is K−STK - S_T: the strike is received at expiry, not today. Discounted at a rate of 4.35%, that future payment is worth less than its face amount. The December put with an 8,800 strike is worth about 160 points less than if it could be exercised today.

With an American put, that discount is a reason to exercise early: receiving the strike today and investing it at the interest rate is worth more than waiting. That is why early exercise makes sense mainly in two cases:

putyesnocallyesnoAmerican optionType?Deep in the money?Exercise nowWaitDividend before expiry?Exercise the day before
Mermaid source
flowchart TB
  A[American option] --> T{Type?}
  T -- put --> P{Deep in the money?}
  P -- yes --> PI[Exercise now]
  P -- no --> NO[Wait]
  T -- call --> D{Dividend before expiry?}
  D -- yes --> CD[Exercise the day before]
  D -- no --> NO
  • Deep in-the-money put: the interest on the strike received today exceeds the remaining time value.
  • Call just before a dividend: the call holder does not receive dividends. If the dividend is larger than the time value, it pays to exercise the day before the ex date to hold the shares and receive it (lesson 1.2.1). Without dividends, exercising an American call early never pays: selling it yields more than exercising it.

For a system that sells American options, this is an operational risk: the counterparty can exercise against the position at any time (assignment), and the seller ends up with shares bought or sold that it did not expect. For a backtest, it means an American option cannot be valued with a European option's formula.

What to watch for in options data

A chain has hundreds of contracts per underlying and day, and most of them barely trade. Liquidity is concentrated in a few:

IN [12]python
jul = cad[cad["vencimiento"] == "2025-07-18"].copy()
jul["redondo"] = np.where(jul["strike"] % 100 == 0,
                          "multiple of 100", "other")
jul["horquilla_pct"] = 100 * (jul["ask"] - jul["bid"]) / jul["medio"]
print(jul.groupby("redondo")["interes_abierto"].mean().round(0))
jul[jul["tipo"] == "call"].set_index("strike").loc[
    [6000, 6400, 6800, 7000, 7200],
    ["bid", "ask", "horquilla_pct"]].round(2)
OUT [12]Where the liquidity is
redondo
multiple of 100    17177.0
other               5873.0
Name: interes_abierto, dtype: float64
           bid     ask  horquilla_pct
strike
6000    432.80  434.70           0.44
6400    144.90  145.60           0.48
6800     14.40   14.70           2.06
7000      1.75    1.90           8.22
7200      0.00    0.15         200.00

Strikes that are multiples of 100 hold about three times more open interest than the ones in between: that is where trading is concentrated. And the spread, measured as a percentage of the premium, shoots up far from the money: on the 7,200 call the bid is zero and the ask, 0.15. On an option like that the “mid price” means nothing, and a backtest that buys and sells at the mid completely underestimates costs.

The most common mistakes when working with options data:

  • Using the last price. A thinly traded option may have had its last trade days ago, with the underlying somewhere else. To value it, use the bid and ask from the same instant.
  • Mixing times. The index at the close and the options at 15:45 are not simultaneous. In parity, one minute of difference with the underlying moving breaks the equality.
  • Forgetting the multiplier or the style. 100 USD per point for most US options, other values in other markets; European or American; cash or physical delivery. It is all in the contract specification.
  • Contract survivorship bias. Historical chains only have the strikes that existed; those added after a strong move were not there before. And expired contracts disappear from many free APIs.

For real chains, yfinance gives the current snapshot of the chain of a US underlying, with no history (lesson 1.3.2). It is not run here because it needs a network connection:

IN [13]python
import yfinance as yf

spy = yf.Ticker("SPY")
print(spy.options[:4])             # available expiries
cadena = spy.option_chain(spy.options[0])
calls, puts = cadena.calls, cadena.puts
calls[["strike", "bid", "ask", "lastPrice", "volume",
       "openInterest", "impliedVolatility", "inTheMoney"]]

option_chain returns two tables, calls and puts, with one row per strike: price, volume, open interest and an implied volatility computed by Yahoo. SPY options are American and on an ETF that pays dividends, so neither exact parity nor the check for puts below intrinsic value applies as is.

Exercises

  1. A long straddle is a call and a put bought with the same strike and expiry. Using the mid prices of the July 6,350 strike, compute the total premium and the two breakevens. How much does the index have to move, in percent, for the position to make money?
Solution
IN [14]python
coste = prima["call"] + prima["put"]
arriba, abajo = K + coste, K - coste
print(f"Total premium: {coste:.2f} points "
      f"({coste * mult:,.0f} USD)")
print(f"Breakevens: {abajo:.2f} and {arriba:.2f}")
print(f"Move needed: {arriba / S - 1:+.2%} "
      f"or {abajo / S - 1:+.2%}")
OUT [14]July at-the-money straddle
Total premium: 315.10 points (31,510 USD)
Breakevens: 6034.90 and 6665.10
Move needed: +4.81% or -5.10%

The straddle pays off if the index moves a lot in either direction, and loses the entire premium if it ends exactly at the strike. The index has to rise more than 4.81% or fall more than 5.10% in six weeks. It is a bet that realized volatility will be higher than what the market has priced into the premiums.

  1. Use put-call parity to infer, for each expiry, which dividend yield qq the chain is pricing (assume rr = 4.354% is known). Hint: with the implied forward, q=r−ln⁡(F/S)/Tq = r - \ln(F/S)/T.
Solution
IN [15]python
dias = (resumen.index - pd.Timestamp("2025-06-02")).days.values
t = dias / 365
q_impl = r - np.log(resumen.values / S) / t
pd.Series(100 * q_impl, index=resumen.index,
          name="implied q (%)").round(3)
OUT [15]Implied dividend
vencimiento
2025-06-20    1.353
2025-07-18    1.350
2025-09-19    1.349
2025-12-19    1.350
Name: implied q (%), dtype: float64

All four expiries give the 1.35% with which the data were generated, with differences of thousandths that come from rounding the premiums to the tick (0.05 or 0.10 points) and that weigh more when TT is small, as in June. With real data, the implied dividend varies by expiry because dividends are not paid out evenly through the year.

  1. A colleague computes the extrinsic value of the options in a chain using the last traded price and finds several out-of-the-money calls with negative extrinsic value. What explanations would you give before thinking of an arbitrage?
Solution

An out-of-the-money call has zero intrinsic value, so a negative extrinsic value means a negative price, which is impossible: the error is in the data or in the calculation. The usual causes: the last price comes from an old trade (or from another contract with the same symbol after an adjustment), the underlying used to compute the intrinsic value is not from the same instant, the strike has been adjusted for a corporate event, or calls and puts have been mixed up. With a simultaneous bid and ask from the same instant as the underlying, the check is repeated and the problem usually disappears.

Footnotes

  1. SPX option specifications on the Cboe page linked in the resources: 100 USD multiplier, European style and cash settlement. ↩

  2. Cboe, on the SPX specifications page: index options (European) are only exercised at expiry; options on stocks and ETFs (American) can be exercised at any time before expiry. In its comparison, ETF options settle by delivery of the shares. ↩

Each answer is folded under its question

Self-assessment

  1. The index is at 6,359.10. A call with a 6,300 strike costs 205.35 points. How much of that premium is extrinsic value?

    • A205.35 points: the whole premium
    • B59.10 points
    • C146.25 points
    • DZero, because it is in the money
    Show answer

    Answer C. 146.25 points

    The call's intrinsic value is what exercising it now would yield: 6,359.10 − 6,300 = 59.10. The rest of the premium, 205.35 − 59.10 = 146.25 points, is extrinsic value: what the market pays for the possibility that the index rises further before expiry.

  2. Which position has an unlimited maximum loss at expiry?

    • ALong call
    • BLong put
    • CShort call
    • DShort put
    Show answer

    Answer C. Short call

    The seller of a call is obliged to pay S − K if the underlying ends above the strike, and the underlying has no ceiling. The short put can also lose a lot, but at most K minus the premium, because the price cannot fall below zero.

  3. Where is the extrinsic value of options with the same expiry highest?

    • AIn the deepest in-the-money ones
    • BIn the deepest out-of-the-money ones
    • CIt is the same at every strike
    • DNear the money, with the strike close to the underlying
    Show answer

    Answer D. Near the money, with the strike close to the underlying

    Far from the strike, the outcome is almost decided: deep in the money the option behaves like the underlying and deep out of the money it is worth almost nothing. The uncertainty about whether it will end in or out of the money, which is what is paid as extrinsic value, is greatest when the strike is close to the current price.

  4. In the FIX chain, several deep in-the-money European puts trade below their intrinsic value. Why is there no arbitrage?

    • ABecause the data are wrong
    • BBecause a European option is only exercised at expiry: the strike is received later and has to be discounted at the interest rate
    • CBecause puts have no intrinsic value
    • DBecause the multiplier is 100
    Show answer

    Answer B. Because a European option is only exercised at expiry: the strike is received later and has to be discounted at the interest rate

    The value of a deep in-the-money European put approaches K·e^(−rT) − S·e^(−qT), not K − S. With rates of 4.35%, receiving the strike in a few months is worth less than receiving it today. An American put could be exercised right away, and that is why it is never worth less than its intrinsic value.

  5. To check put-call parity on a real chain, which prices should you use?

    • ABid and ask: C_bid − P_ask and C_ask − P_bid bound what can actually be done
    • BThe last traded price of the call and the put
    • COnly the call's mid price
    • DThe index closing price
    Show answer

    Answer A. Bid and ask: C_bid − P_ask and C_ask − P_bid bound what can actually be done

    The last price can be hours old and not simultaneous for the call and the put. What is executable is the bid and the ask: buying the call and selling the put costs C_ask − P_bid; the opposite brings in C_bid − P_ask. If the theoretical value falls between the two, there is no arbitrage after spreads.

Key points

  • A call gives the right to buy and a put the right to sell, at a fixed price (the strike) until a date. The buyer pays the premium; the seller receives it and takes on the obligation.
  • The premium splits into intrinsic value (what exercising now would yield) and extrinsic value (what waiting is worth). Extrinsic value peaks near the money and grows with time to expiry.
  • Time value is not lost at a constant pace: it grows roughly with the square root of time to expiry, so the last month takes the most.
  • In a European chain, C − P = e^(−rT)(F − K): put-call parity links calls and puts with the same strike and is used to validate the data and to infer the forward.
  • An American option can be exercised any day and is never worth less than its intrinsic value; a European one, only at expiry, and it can be worth less.
  • Options data mislead easily: stale last prices, wide spreads far from the money, multipliers and exercise styles that differ from contract to contract.

Resources

Data