Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Black–Scholes Surface Explorer

Interactive 3D visualization of European option prices, first- and second-order Greeks, and a realized-volatility pipeline over ten years of daily equity data.

Python Streamlit Plotly NumPy SciPy

Option risk is easy to state as a formula and hard to feel. This project makes the Black–Scholes value function and its derivatives tangible: every quantity is rendered as a rotatable surface over the two dimensions a desk actually trades — spot and time to expiry — so that the geometry of convexity, decay and vol sensitivity is visible rather than tabulated.

Black-Scholes call option price surface over underlying price and days to expiration
The call value surface. The kinked payoff at expiry, smoothed by time value — every other surface in this repository is a derivative of this one.


Contents


What's in here

File Role
black-scholes.py Risk surface app. Long call and long put value, Delta (call/put), Gamma (long and short), Vega, and the second-order cross-Greeks Vanna and Zomma. The put is plotted against volatility × time rather than spot × time, isolating the vega dimension.
3d.py Narrative app. Walks from the terminal payoff — the "hockey stick" — through the smoothed price surface to Delta, Gamma, Theta, Vega and Rho, showing how each Greek is a slope or curvature of the surface that precedes it.
volatility.py Data pipeline. Close-to-close log returns → rolling standard deviation → annualized realized volatility, appended as a Volatility column.
data/ Ten years of daily bars for six US mega-caps, each already carrying its realized-volatility column.

Both apps are parameter-driven: strike, volatility and the risk-free rate are sidebar controls, and the full 80 × 80 lattice is re-evaluated on every interaction. The dataset exists to anchor those inputs in observed market behavior rather than guesswork — a 30-day realized vol of ≈ 13% for MSFT and ≈ 33% for NVDA at the end of the sample is a concrete reminder of how much the volatility slider is really doing.

Gallery

Long put price as a function of volatility and time to expiration

Put value vs. volatility & time
Monotone in both arguments: time and volatility widen the terminal distribution in the same direction.

Call delta surface over underlying price and days to expiration

Call Delta
The expiry step function, softened by time value. The cliff sharpens as the surface approaches expiration.

Short gamma surface showing negative convexity near the strike

Short Gamma
The option seller's position: a well of negative convexity, deepest at-the-money in the final days.

Vega surface peaking at-the-money at long maturities

Vega
Peaks near at-the-money and grows with the square root of time — the opposite corner of the grid from Gamma.

Vanna surface changing sign across the strike

Vanna
The sign flip at d₂ = 0 in plain view: positive out-of-the-money, negative in-the-money.

Zomma surface showing where gamma is amplified or eroded by a volatility shift

Zomma
How stable Gamma is under a volatility shift. The sign change traces the locus d₁d₂ = 1.

Surfaces are rendered live in the browser and are fully rotatable; the stills above are captured from black-scholes.py at its default parameters (strike $50, volatility 30%, risk-free rate 1%).

Quickstart

python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt

Launch either app:

streamlit run black-scholes.py
streamlit run 3d.py

Regenerate a volatility column from a raw daily-bar export:

python volatility.py

Mathematical framework

Under the Black–Scholes–Merton assumptions the underlying follows a geometric Brownian motion under the risk-neutral measure $\mathbb{Q}$,

$$dS_t = r S_t,dt + \sigma S_t,dW_t^{\mathbb{Q}},$$

and any contingent claim $V(S,t)$ satisfies the pricing PDE

$$\frac{\partial V}{\partial t} + \tfrac{1}{2}\sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + rS\frac{\partial V}{\partial S} - rV = 0 .$$

Written in Greeks, this is the identity that governs every surface in the repository:

$$\Theta + \tfrac{1}{2}\sigma^2 S^2 \Gamma + rS\Delta = rV .$$

It is the reason theta and gamma appear as mirror images: a long option position is paid convexity and charged time, and the PDE fixes the exchange rate between the two.

With $\tau = T - t$ the time to expiry, the closed-form European solutions are

$$C = S,N(d_1) - Ke^{-r\tau}N(d_2), \qquad P = Ke^{-r\tau}N(-d_2) - S,N(-d_1),$$

$$d_1 = \frac{\ln(S/K) + \left(r + \tfrac{1}{2}\sigma^2\right)\tau}{\sigma\sqrt{\tau}}, \qquad d_2 = d_1 - \sigma\sqrt{\tau},$$

where $N(\cdot)$ and $\varphi(\cdot)$ are the standard normal CDF and PDF (scipy.stats.norm.cdf / .pdf).

Greeks implemented

Greek Order Measures Closed form (call)
Delta $\Delta$ 1st, $\partial_S$ Directional exposure $N(d_1)$, and $N(d_1)-1$ for the put
Gamma $\Gamma$ 2nd, $\partial_{SS}$ Convexity of the position $\dfrac{\varphi(d_1)}{S\sigma\sqrt{\tau}}$
Vega $\mathcal{V}$ 1st, $\partial_\sigma$ Sensitivity to implied vol $S\varphi(d_1)\sqrt{\tau}$
Theta $\Theta$ 1st, $\partial_t$ Time decay $-\dfrac{S\varphi(d_1)\sigma}{2\sqrt{\tau}} - rKe^{-r\tau}N(d_2)$
Rho $\rho$ 1st, $\partial_r$ Rate sensitivity $K\tau e^{-r\tau}N(d_2)$
Vanna 2nd, $\partial_{S\sigma}$ Drift of Delta under a vol move $-\varphi(d_1)\dfrac{d_2}{\sigma}$
Zomma 3rd, $\partial_{SS\sigma}$ Stability of Gamma under a vol move $\Gamma,\dfrac{d_1 d_2 - 1}{\sigma}$

Quoting conventions used in the code, chosen to match how a desk reads them: Vega and Rho are scaled by $0.01$ (value change per one point of volatility or rates), Theta is divided by 365 (value decay per calendar day), and Gamma and Vega are plotted once because they are identical for calls and puts — a consequence of put–call parity, whose difference $C - P = S - Ke^{-r\tau}$ is linear in $S$ and independent of $\sigma$. The short-gamma surface is the exact reflection $-\Gamma$: the same geometry seen from the seller's side of the trade.

Reading the surfaces

The visualizations are built around four structural facts that repay rotation of the plots:

  • Gamma and Vega peak in different corners. Gamma is largest at-the-money close to expiry; Vega is largest at-the-money far from expiry. Both are "long optionality", but one is a bet on realized movement and the other on the price of anticipated movement — and no single expiry maximizes both.
  • Theta is the price of Gamma. The PDE identity above forces the decay surface to be steepest exactly where the curvature surface is tallest. A long gamma book pays for its convexity daily; a short gamma book collects that payment and inherits negative convexity in exchange.
  • Vanna changes sign at $d_2 = 0$, i.e. at $S = K e^{-(r-\sigma^2/2)\tau}$. Where a call is out-of-the-money ($d_2 < 0$) a rise in volatility increases Delta; where it is in-the-money ($d_2 > 0$) the same rise decreases it. A pure volatility move therefore re-hedges a book that has not traded a single share — the mechanism behind vanna-volga pricing and behind the spot-vol flows dealers recycle when skew shifts.
  • Zomma vanishes on $d_1 d_2 = 1$. Where $d_1 d_2 < 1$ — the at-the-money region — a rise in volatility erodes Gamma; out in the wings, where $d_1 d_2 > 1$, it amplifies Gamma instead. It is the cleanest answer to the practical question "how stable is my convexity if the vol surface shifts?".

Realized-volatility pipeline

volatility.py converts a raw daily-bar export into an annualized realized-volatility series:

  1. Parse dates, sort ascending (a rolling window is meaningless on unordered data), coerce prices to numeric.
  2. Continuously-compounded returns, $r_t = \ln\left(P_t / P_{t-1}\right)$ — additive across time and symmetric in up/down moves, unlike simple returns.
  3. Rolling sample standard deviation over a 30-observation window (configurable via window).
  4. Annualize by $\sqrt{252}$, the conventional count of US trading days:

$$\hat{\sigma}_{\text{ann}} = \sqrt{252};\cdot; \operatorname{sd}\left(r_{t-29},\dots,r_t\right).$$

The estimator is deliberately the textbook close-to-close one, so its behavior is transparent: the first 29 observations of each series are undefined, and because the window is short it responds quickly to regime changes at the cost of sampling noise. data/*.csv are the pipeline's output; the script's default input is a raw export in the same schema (Investing.com-style, %m/%d/%Y dates) and is not committed.

Dataset

Daily bars, Jun 23, 2015 → Jun 23, 2025, prices split-adjusted as delivered by the source:

File Ticker Observations
data/AAPL.csv Apple 2,515
data/MSFT.csv Microsoft 2,515
data/NVDA.csv NVIDIA 2,515
data/META.csv Meta Platforms 2,515
data/AMZN.csv Amazon 2,514
data/GOOG.csv Alphabet 2,514
data/test.csv AAPL slice 163 (fixture for quick iteration)

Schema: Date, Price, Open, High, Low, Vol., Change %, Volatility — the final column produced by the pipeline above, nan over the window's burn-in period.

Implementation notes

  • Fully vectorized. Each surface is a single NumPy expression over an $80 \times 80$ meshgrid (6,400 lattice points), evaluated without a Python-level loop, so a slider move recomputes the entire risk surface within one Streamlit rerun.
  • Singularity handling at $\tau \to 0$. $d_1$, $\Gamma$ and $\Theta$ all diverge as expiry approaches. The grids start at one day and an $\varepsilon$ regularizer guards the denominators, keeping the surfaces finite where the closed form is not.
  • Day-count conventions are kept distinct. Option time is calendar-based ($\tau = \text{days}/365$, matching how expiries are quoted); realized volatility is annualized on 252 trading days. Conflating the two is a common and quietly material error.
  • Layout factored once. A shared create_layout helper fixes camera, aspect ratio and axis titles across every plot, so surfaces are visually comparable rather than each auto-scaled to its own frame.

Assumptions and limitations

Stated explicitly, because the gap between this model and traded markets is the interesting part:

  • Constant volatility. A single $\sigma$ across all strikes and maturities. Real markets exhibit a volatility smile/skew; the surfaces here are the flat-vol baseline against which that skew is measured.
  • No dividends. The Merton $q$ term is omitted, so prices are exact for non-dividend-paying underlyings and biased for the dividend payers in data/. Adding $q$ amounts to substituting $Se^{-q\tau}$ for $S$ throughout.
  • European exercise. No early-exercise premium, so the surfaces understate American puts.
  • Frictionless and continuous. No bid–ask spread, no transaction costs, no borrow cost, continuous trading and infinite divisibility. Log-normal returns also under-weight tails relative to observed equity returns.
  • Constant, known risk-free rate, flat across the term structure.

Roadmap

  • Invert the pricer for implied volatility (Newton–Raphson on Vega, bisection fallback) and fit a smile to listed chains.
  • Add the dividend yield $q$ and an American binomial/LSMC comparator to quantify the early-exercise premium.
  • Stochastic volatility (Heston) and local volatility (Dupire) surfaces alongside the Black–Scholes baseline.
  • Delta-hedging simulator over the shipped price history: realized P&L as the gamma–theta trade-off, decomposed into hedging error and the realized-vs-implied vol spread.
  • Range-based volatility estimators (Parkinson, Garman–Klass, Yang–Zhang) — the OHLC columns are already in the data and are several times more efficient than close-to-close.

Built with Streamlit, Plotly, NumPy, SciPy and pandas.

About

Interactive 3D Black–Scholes explorer: price, Delta, Gamma, Theta, Vega, Rho, Vanna and Zomma as rotatable surfaces, plus a realized-volatility pipeline over 10 years of daily equity data.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages