Alphanume

Insights

How to Compute Option Greeks in Python

Alphanume Team · June 8, 2026

Analytic Greeks straight from Black-Scholes.

Understanding how an option's value responds to changes in the underlying is the foundation of any serious options position. This tutorial implements option greeks python practitioners actually use — delta, gamma, vega, theta, and rho — derived directly from the closed-form Black-Scholes model using scipy.stats.norm. If you want to see the pricing side first, work through Black-Scholes in Python before continuing here, and keep the options pricing calculator open to sanity-check your numbers as you go. The full reference treatment of every Greek and its interpretation lives in how to calculate option Greeks.

The Black-Scholes d1 and d2 setup

Every analytic Greek is a derivative of the Black-Scholes price formula, so they all share the same two intermediate quantities: d1 and d2. Given spot price S, strike K, time to expiry in years T, continuously compounded risk-free rate r, and annualised implied volatility sigma:

d1 = ( ln(S/K) + (r + ½σ2)T ) / ( σ√T )

d2 = d1 − σ√T

The cumulative standard normal N(x) appears in the price formula and in delta and rho. The standard normal PDF n(x) — the derivative of N(x) — appears in gamma, vega, and theta. In SciPy these are norm.cdf(x) and norm.pdf(x) respectively, both from scipy.stats.

A few inputs to keep straight before writing any code: T is a fraction of a year, not a raw day count — divide calendar days by 365 (or 252 for trading days, depending on your convention). sigma is expressed as a decimal, so 20% implied vol is 0.20. The risk-free rate is similarly a decimal annual rate.

Implementing each Greek

Delta measures the rate of change of option price with respect to the underlying. For a European call it is simply N(d1); for a put it is N(d1) - 1 (equivalently -N(-d1)). Delta lives in [0, 1] for calls and [-1, 0] for puts, and it doubles as a rough probability proxy — a 0.50-delta call is approximately at-the-money.

Gamma is the second derivative of price with respect to the underlying — equivalently the rate of change of delta. It is identical for calls and puts: n(d1) / (S × σ × √T). Gamma is always positive and peaks for at-the-money options near expiry.

Vega measures sensitivity to a one-unit move in implied volatility. The formula is S × n(d1) × √T. Here "one unit" means a move of 1.00 in sigma — that is, volatility going from 0.20 to 1.20, which is obviously not useful. Practitioners almost always divide by 100 so that vega represents the change in value per one percentage-point move in vol (0.20 to 0.21). Like gamma, vega is the same for calls and puts.

Theta is the time decay — how much value the option loses per unit of time as expiry approaches. The closed-form expressions differ for calls and puts and carry a negative sign because options lose time value. The raw formula is in per-year terms; divide by 365 to get the familiar per-calendar-day figure.

Rho is the sensitivity to the risk-free rate. For a call: K × T × e<sup>-rT</sup> × N(d2). For a put: -K × T × e<sup>-rT</sup> × N(-d2). Like vega, the raw output is per unit of r; dividing by 100 gives the change per basis point.

The greeks() function

The implementation below bundles all five Greeks into a single function that returns a plain dictionary. Keeping everything in one place makes it straightforward to tabulate Greeks across a strip of strikes or a range of expiries.

import numpy as np
from scipy.stats import norm


def greeks(S, K, T, r, sigma, kind="call"):
    """
    Compute analytic Black-Scholes Greeks for a European option.

    Parameters
    ----------
    S     : float  Current underlying price
    K     : float  Strike price
    T     : float  Time to expiry in years (e.g. 30/365)
    r     : float  Continuously compounded risk-free rate (decimal)
    sigma : float  Annualised implied volatility (decimal, e.g. 0.20)
    kind  : str    'call' or 'put'

    Returns
    -------
    dict with keys: delta, gamma, vega, theta, rho
    Notes
    -----
    - Vega is per 1% move in vol  (divide raw by 100)
    - Theta is per calendar day   (divide raw by 365)
    - Rho   is per 1% move in r   (divide raw by 100)
    - Assumes European exercise, no dividends.
    """
    kind = kind.lower()
    if kind not in ("call", "put"):
        raise ValueError("kind must be 'call' or 'put'")
    if T <= 0:
        raise ValueError("T must be positive")

    d1 = (np.log(S / K) + (r + 0.5 * sigma ** 2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)

    nd1  = norm.pdf(d1)          # standard normal PDF at d1
    Nd1  = norm.cdf(d1)          # CDF at d1
    Nd2  = norm.cdf(d2)          # CDF at d2
    Nnd1 = norm.cdf(-d1)         # CDF at -d1  (= 1 - Nd1)
    Nnd2 = norm.cdf(-d2)         # CDF at -d2

    discount = np.exp(-r * T)

    # ---- delta ----
    if kind == "call":
        delta = Nd1
    else:
        delta = Nd1 - 1.0        # equivalent to -norm.cdf(-d1)

    # ---- gamma (same for call and put) ----
    gamma = nd1 / (S * sigma * np.sqrt(T))

    # ---- vega: per 1% move in sigma ----
    vega_raw = S * nd1 * np.sqrt(T)
    vega = vega_raw / 100.0

    # ---- theta: per calendar day ----
    decay = -(S * nd1 * sigma) / (2.0 * np.sqrt(T))
    if kind == "call":
        theta_raw = decay - r * K * discount * Nd2
    else:
        theta_raw = decay + r * K * discount * Nnd2
    theta = theta_raw / 365.0

    # ---- rho: per 1% move in r ----
    if kind == "call":
        rho_raw = K * T * discount * Nd2
    else:
        rho_raw = -K * T * discount * Nnd2
    rho = rho_raw / 100.0

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

Worked example

Consider a 30-day at-the-money call on a stock trading at 100, with a strike of 100, 20% implied vol, and a 5% risk-free rate.

S     = 100.0    # spot price
K     = 100.0    # strike
T     = 30 / 365 # ~30 calendar days to expiry
r     = 0.05     # 5% annual risk-free rate
sigma = 0.20     # 20% implied volatility

call_greeks = greeks(S, K, T, r, sigma, kind="call")
put_greeks  = greeks(S, K, T, r, sigma, kind="put")

print("=== 30-day ATM Call ===")
for name, val in call_greeks.items():
    print(f"  {name:>6}: {val:.6f}")

print("\n=== 30-day ATM Put ===")
for name, val in put_greeks.items():
    print(f"  {name:>6}: {val:.6f}")

Running this you should see output along the lines of:

=== 30-day ATM Call ===
   delta:  0.524933
   gamma:  0.137870
    vega:  0.112684
   theta: -0.053024
     rho:  0.010955

=== 30-day ATM Put ===
   delta: -0.475067
   gamma:  0.137870
    vega:  0.112684
   theta: -0.052617
     rho: -0.010576

A few things to observe. The call delta of ~0.52 and the put delta of ~-0.48 do not sum to exactly zero because the option is not precisely at-the-money once you account for the cost-of-carry term in d1; they sum to -1 by put-call parity when you include the sign correctly. Gamma and vega are identical for the call and put — this is expected from put-call parity. Theta is slightly more negative for the call here because the call has a higher absolute value, so it has more time value to decay.

Sign and scaling conventions — common mistakes

Getting the formulas right is only half the battle. The conventions for units and signs trip up practitioners regularly.

Vega units. The raw formula gives vega per unit of sigma. A "unit" of vol is 1.00 — moving implied vol from 0.20 to 1.20 — which is economically meaningless. Always divide by 100 so vega is expressed per percentage-point move (0.20 to 0.21). Some trading systems quote vega per vol point (0.20 to 0.30), which would be a division by 10. Know which convention your system expects before plugging numbers in.

Theta units. The raw formula is annualised — the daily decay you actually care about requires dividing by 365 (or 252 if you are working in trading days). Forgetting this step overstates theta by roughly two orders of magnitude and leads to dramatic mispricing in any P&L attribution.

Put theta sign. Theta is almost always negative — the option loses value as time passes — but it can turn positive for deep in-the-money puts near expiry in low-rate environments. The formula handles this correctly; do not hard-code a sign.

Put delta sign. Put delta is negative. If your downstream code accumulates a portfolio delta, make sure put deltas are not accidentally treated as positive, which would double-count your hedge rather than offset it.

Rho scale. Rho is the least significant Greek for short-dated options but can matter for long-dated LEAPS. The per-basis-point convention (divide by 100) keeps the number in the same ballpark as the option premium, which makes position-level aggregation easier to sanity-check.

Assumptions and limitations

Everything above assumes a European-style option on a non-dividend-paying underlying, priced under the constant-volatility Black-Scholes framework. These are serious assumptions in practice. American options require numerical methods — a binomial tree or finite-difference PDE — because early exercise can be optimal. Dividends are typically handled by replacing the spot price S with S × e<sup>-qT</sup> where q is the continuous dividend yield, but discrete dividend handling is more involved. Volatility is not constant across strikes and expiries — the implied vol surface is real and the analytic Greeks computed at a single sigma are at best a local linear approximation. For those extensions the formulas here remain the right starting point; they are exact within the model and correct implementations of them are a prerequisite for any more sophisticated approach.