Options, Priced Against Reality¶

Open In Colab

Black-Scholes and the Greeks anchored to a real fetched spot price, not a made-up number.

Part 28 of 35 in the ServLoci algo/options trading notebook series — full index in notebooks/README.md.

Setup — no broker account needed¶

!pip install -q yfinance

import yfinance as yf
import pandas as pd
import numpy as np

# No broker account, no API key, no ServLoci setup needed for this chapter —
# yfinance reads public end-of-day data from Yahoo Finance. Indian tickers take
# an ".NS" suffix (NSE) or ".BO" (BSE); index tickers are prefixed with "^"
# (^NSEI = Nifty 50, ^BSESN = Sensex, ^GSPC = S&P 500).
NSE_TICKER = "RELIANCE.NS"
US_TICKER = "AAPL"
INDEX_TICKER = "^NSEI"

print("yfinance", yf.__version__)

Most options tutorials price a number nobody checked¶

Open almost any options-Greeks tutorial and you'll find spot = 100, strike = 105 — clean, round, and disconnected from anything trading right now. That's fine for teaching the shape of a formula, but it hides the part that actually matters in practice: an option's price and Greeks are only as good as the inputs you feed them, and two of those inputs — spot and volatility — are things you should be measuring, not guessing.

This notebook fetches a real spot price with yfinance, computes a real historical volatility from real trailing returns, and only then calls the same bs_price() / greeks() pricer used in notebook 06 — so every number below is anchored to something that existed in the market a moment ago, not a textbook placeholder.

spot_row = yf.Ticker(NSE_TICKER).history(period="5d")
spot = float(spot_row["Close"].iloc[-1])
as_of = spot_row.index[-1].date()
print(f"{NSE_TICKER} last close: {spot:.2f} (as of {as_of})")

us_row = yf.Ticker(US_TICKER).history(period="5d")
us_spot = float(us_row["Close"].iloc[-1])
print(f"{US_TICKER} last close: {us_spot:.2f} (as of {us_row.index[-1].date()})")

Historical volatility vs. implied volatility — not the same number¶

There are two honest ways to get a volatility input, and they answer different questions:

  • Historical (realized) volatility looks backward. It's the standard deviation of past returns, annualized — a measured fact about what already happened. It's what we compute below.
  • Implied volatility looks forward. It's backed out of an actual option's market price (notebook 12 does this via bisection) — the market's current forecast, which can differ sharply from recent realized volatility right before an event like results or a policy announcement.

Feeding historical volatility into Black-Scholes gives you a model price consistent with recent behavior, not a prediction of where the option will actually trade — if the market is pricing in an upcoming catalyst, implied volatility will run ahead of historical volatility, and the gap between the two is itself useful information, not noise.

import numpy as np

def annualized_realized_vol(history, window=30):
    log_ret = np.log(history["Close"] / history["Close"].shift(1)).dropna()
    return float(log_ret.tail(window).std() * np.sqrt(252))

nse_hist = yf.Ticker(NSE_TICKER).history(period="6mo")
hv_30 = annualized_realized_vol(nse_hist, window=30)
hv_90 = annualized_realized_vol(nse_hist, window=90)
print(f"{NSE_TICKER} 30-day realized vol: {hv_30:.1%}")
print(f"{NSE_TICKER} 90-day realized vol: {hv_90:.1%}")
from scipy.stats import norm
import math

def bs_price(opt_type, spot, strike, t_years, vol, rate=0.065):
    if t_years <= 0 or vol <= 0:
        return max(spot - strike, 0) if opt_type == "CE" else max(strike - spot, 0)
    d1 = (math.log(spot / strike) + (rate + vol * vol / 2) * t_years) / (vol * math.sqrt(t_years))
    d2 = d1 - vol * math.sqrt(t_years)
    if opt_type == "CE":
        return spot * norm.cdf(d1) - strike * math.exp(-rate * t_years) * norm.cdf(d2)
    return strike * math.exp(-rate * t_years) * norm.cdf(-d2) - spot * norm.cdf(-d1)

def greeks(opt_type, spot, strike, t_years, vol, rate=0.065):
    if t_years <= 0 or vol <= 0:
        return {"delta": 0, "gamma": 0, "theta": 0, "vega": 0}
    d1 = (math.log(spot / strike) + (rate + vol * vol / 2) * t_years) / (vol * math.sqrt(t_years))
    d2 = d1 - vol * math.sqrt(t_years)
    nd1 = norm.pdf(d1)
    delta = norm.cdf(d1) if opt_type == "CE" else norm.cdf(d1) - 1
    gamma = nd1 / (spot * vol * math.sqrt(t_years))
    vega = (spot * nd1 * math.sqrt(t_years)) / 100  # per 1% vol move
    term1 = -(spot * nd1 * vol) / (2 * math.sqrt(t_years))
    if opt_type == "CE":
        theta = (term1 - rate * strike * math.exp(-rate * t_years) * norm.cdf(d2)) / 365
    else:
        theta = (term1 + rate * strike * math.exp(-rate * t_years) * norm.cdf(-d2)) / 365
    return {"delta": delta, "gamma": gamma, "theta": theta, "vega": vega}

# Round spot to the nearest 50 to pick a realistic near-the-money strike.
strike = round(spot / 50) * 50
t_years = 7 / 365  # a hypothetical weekly expiry, 7 calendar days out

ce_price = bs_price("CE", spot, strike, t_years, hv_30)
ce_greeks = greeks("CE", spot, strike, t_years, hv_30)
print(f"{NSE_TICKER} spot {spot:.2f}, strike {strike}, vol {hv_30:.1%} (30-day realized)")
print("Model CE price:", round(ce_price, 2))
print("Model CE greeks:", {k: round(v, 4) for k, v in ce_greeks.items()})

Reading the Greeks against numbers you just measured¶

  • Delta — with spot near the strike (we rounded to the nearest 50 specifically to land close to at-the-money), expect delta near 0.5 for the call. Re-run this notebook on a day when the stock has moved and watch delta shift toward 0 (deep OTM) or 1 (deep ITM) — that's delta doing its job as a moneyness gauge, not an abstract Greek in a table.
  • Gamma — highest exactly where we are, ATM, with 7 days to expiry. If you change t_years to something much larger, gamma drops noticeably: gamma concentrates near expiry, which is why index-option desks watch gamma exposure most closely in the final days before a weekly settlement.
  • Theta — this is a daily decay figure, in the same currency units as spot. Multiply by the lot size and by days held to see the actual carry a short seller is collecting (or a long buyer is bleeding) — a much more concrete number than "options decay over time."
  • Vega — priced off hv_30, our historical estimate. If you separately pulled a real implied volatility for this strike (notebook 12) and it's meaningfully higher than hv_30, that gap is the market pricing in more uncertainty than recent history alone would suggest — often the case right before earnings or a macro event.

None of this makes Black-Scholes correct — it's still a model with constant-volatility, continuous-trading assumptions that real markets violate. What real spot and real historical volatility buy you is a pricer that's wrong in a known, measurable way, instead of wrong in a way you can't even characterize because the inputs were invented.


« Previous: Technical Indicators, Tested Honestly
Next: Backtesting Without Fooling Yourself »

Try the concepts above interactively: Options Strategy Builder · Docs · Get your static IP