Interactive 3D visualization of European option prices, first- and second-order Greeks, and a realized-volatility pipeline over ten years of daily equity data.
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.
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.
- What's in here
- Gallery
- Quickstart
- Mathematical framework
- Greeks implemented
- Reading the surfaces
- Realized-volatility pipeline
- Dataset
- Implementation notes
- Assumptions and limitations
- Roadmap
| 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.
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%).
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txtLaunch either app:
streamlit run black-scholes.pystreamlit run 3d.pyRegenerate a volatility column from a raw daily-bar export:
python volatility.pyUnder the Black–Scholes–Merton assumptions the underlying follows a geometric Brownian motion under the risk-neutral measure
and any contingent claim
Written in Greeks, this is the identity that governs every surface in the repository:
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
where scipy.stats.norm.cdf / .pdf).
| Greek | Order | Measures | Closed form (call) |
|---|---|---|---|
|
Delta |
1st, |
Directional exposure |
|
|
Gamma |
2nd, |
Convexity of the position | |
|
Vega |
1st, |
Sensitivity to implied vol | |
|
Theta |
1st, |
Time decay | |
|
Rho |
1st, |
Rate sensitivity | |
| Vanna | 2nd, |
Drift of Delta under a vol move | |
| Zomma | 3rd, |
Stability of Gamma under a vol move |
Quoting conventions used in the code, chosen to match how a desk reads them: Vega and Rho are scaled by
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?".
volatility.py converts a raw daily-bar export into an annualized realized-volatility series:
- Parse dates, sort ascending (a rolling window is meaningless on unordered data), coerce prices to numeric.
- 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. - Rolling sample standard deviation over a 30-observation window (configurable via
window). - Annualize by
$\sqrt{252}$ , the conventional count of US trading days:
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.
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.
-
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_layouthelper fixes camera, aspect ratio and axis titles across every plot, so surfaces are visually comparable rather than each auto-scaled to its own frame.
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 indata/. 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.
- 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.





