Greeks: Delta, Gamma, Theta, Vega, Rho

The Greeks are the partial derivatives of the Black-Scholes price (chapter 118) with respect to each input — each answers "how much does the option's price change if this one thing changes, holding everything else constant?"

import math
from scipy.stats import norm

def compute_greeks(S: float, K: float, T: float, r: float, sigma: float, option_type: str = "CE") -> dict:
    if T <= 0:
        return {"delta": 0, "gamma": 0, "theta": 0, "vega": 0, "rho": 0}

    d1 = (math.log(S / K) + (r + sigma**2 / 2) * T) / (sigma * math.sqrt(T))
    d2 = d1 - sigma * math.sqrt(T)
    pdf_d1 = norm.pdf(d1)

    if option_type == "CE":
        delta = norm.cdf(d1)
        theta = (-S * pdf_d1 * sigma / (2 * math.sqrt(T)) - r * K * math.exp(-r * T) * norm.cdf(d2)) / 365
        rho = K * T * math.exp(-r * T) * norm.cdf(d2) / 100
    else:
        delta = norm.cdf(d1) - 1
        theta = (-S * pdf_d1 * sigma / (2 * math.sqrt(T)) + r * K * math.exp(-r * T) * norm.cdf(-d2)) / 365
        rho = -K * T * math.exp(-r * T) * norm.cdf(-d2) / 100

    gamma = pdf_d1 / (S * sigma * math.sqrt(T))
    vega = S * pdf_d1 * math.sqrt(T) / 100

    return {"delta": delta, "gamma": gamma, "theta": theta, "vega": vega, "rho": rho}

Reading each Greek in plain terms

GreekMeasuresTypical range (near ATM)Practical meaning
DeltaPrice change per ₹1 move in spot0 to 1 (calls), -1 to 0 (puts)~0.5 at ATM; also roughly approximates probability of expiring ITM
GammaRate of change of Delta itselfHighest ATM, near expiryHigh gamma = Delta changes fast = position risk changes fast
ThetaPrice decay per day (time passing)Negative for long optionsThe cost of holding a long option position — accelerates near expiry
VegaPrice change per 1% change in IVHighest ATM, far from expiryLong options gain from rising IV, lose from falling IV, independent of direction
RhoPrice change per 1% change in ratesSmall for short-dated optionsUsually the least relevant Greek for short-term Indian index options

Why these matter for practical position management, not just theory

def portfolio_delta_exposure(positions: list[dict]) -> float:
    """positions: [{quantity, delta, lot_size}, ...]"""
    return sum(p["quantity"] * p["delta"] * p["lot_size"] for p in positions)

A trader holding "5 lots of NIFTY calls" doesn't actually know their real directional exposure without converting through Delta — 5 lots of a deep-OTM call (delta ~0.1) is a very different directional bet than 5 lots of an ATM call (delta ~0.5), despite identical lot count. This is the foundation for chapter 122's portfolio-level Greeks aggregation.

Theta decay is not linear — it accelerates

def theta_over_time(S, K, r, sigma, option_type, days_range: list[int]) -> pd.DataFrame:
    results = []
    for days in days_range:
        greeks = compute_greeks(S, K, days_to_years(days), r, sigma, option_type)
        results.append({"days_to_expiry": days, "theta": greeks["theta"]})
    return pd.DataFrame(results)

Running this shows theta decay is roughly flat far from expiry and sharply accelerates in the final week — directly relevant to why option sellers often prefer holding through the final days before expiry (faster theta capture) while option buyers face the opposite headwind.

Sanity-checking your Greeks against your broker's own display

def sanity_check_greeks(computed: dict, broker_displayed: dict, tolerance: float = 0.05):
    for greek, value in computed.items():
        if greek in broker_displayed:
            assert abs(value - broker_displayed[greek]) < tolerance, f"{greek} mismatch"

Small discrepancies against your broker's displayed Greeks are normal (different IV inputs, slightly different rate assumptions) — large discrepancies mean a bug in your formula or a wrong input (commonly: T computed in days instead of years, or sigma as a percentage like 20 instead of a decimal like 0.20).

Next: 120 — Implied volatility (Newton-Raphson solve)