A quantitative-trading-firm website with a real, working options pricing engine embedded as its centerpiece: a FastAPI backend that prices European options with the Black-Scholes closed-form model and a Monte Carlo simulation, backed by an optional compiled C++ execution path with an automatic Python fallback.
This README explains what the program does, the math behind it, and how every piece fits together — read it top to bottom and you should understand the whole project without opening the code.
- What this project is
- Topics covered
- The math
- How the code maps to the math
- Project structure
- Running it
- API reference
- Deployment
- License
There are two things bundled together:
- A pricing engine — implemented three times over (C++, Python/NumPy, and a JavaScript fallback in the browser), all computing the same math, so the result is identical no matter which one answers your request.
- A website built around that engine — a "pricing terminal" widget where you type in option contract terms and get live prices, Greeks, and a convergence chart back from the API.
The point of the site is to make the math and the code that implements it directly inspectable and interactive, rather than a static writeup.
If you want to use this repo as a learning reference, here's what it touches:
| Area | Where |
|---|---|
| Options pricing theory (Black-Scholes) | backend/services/bs_pricer.py |
| Monte Carlo methods / stochastic simulation | backend/services/mc_pricer.py |
| Geometric Brownian Motion | mc_pricer.py, options.cpp |
| Option Greeks (Delta, Gamma, Vega, Theta, Rho) | bs_pricer.py::greeks() |
| Numerical vs. analytical convergence | mc_pricer.py::convergence_series() |
| C++ for performance-critical numerics | backend/cpp_src/options.cpp |
| Calling compiled binaries from Python (subprocess) | services/cpp_wrapper.py |
| REST API design with FastAPI + Pydantic | routers/pricing.py, schemas/pricing.py |
| Graceful engine fallback (C++ → Python → JS) | cpp_wrapper.py, script.js |
| Multi-stage Docker builds | Dockerfile |
| Cloud deployment (Render) | render.yaml, build.sh, start.sh |
| Frontend data visualization (canvas charting) | frontend/script.js |
The Black-Scholes model gives a closed-form price for a European option — no simulation needed, just plug numbers into a formula. It assumes the underlying asset price follows Geometric Brownian Motion under the risk-neutral measure:
dS_t = r · S_t dt + σ · S_t dW_t
Where:
S_t— the asset price at timetr— the risk-free interest rateσ(sigma) — volatility of the assetW_t— a Wiener process (Brownian motion / random walk)
Given:
S— current spot price of the underlyingK— strike pricer— risk-free rateσ— volatilityT— time to maturity, in years
the call and put prices are:
Call price: C = S · N(d1) − K · e^(−rT) · N(d2)
Put price: P = K · e^(−rT) · N(−d2) − S · N(−d1)
where N(x) is the standard normal cumulative distribution function
(the probability a standard normal random variable is ≤ x), and:
d1 = [ ln(S/K) + (r + σ²/2) · T ] / (σ · √T)
d2 = d1 − σ · √T
Intuition: d1 and d2 measure how far in-the-money the option is
expected to be, in units of volatility-adjusted standard deviations. N(d2)
is (approximately) the risk-neutral probability the option finishes
in-the-money; N(d1) weights that by the expected payoff.
Implemented in: backend/services/bs_pricer.py (using scipy.stats.norm)
and backend/cpp_src/options.cpp (using erfc — the complementary error
function, since N(x) = 0.5 · erfc(−x / √2)).
Instead of a formula, Monte Carlo simulates many possible future paths of the stock price and averages the payoff. It's slower and only approximate, but it generalizes to option types that don't have a clean closed-form solution (this project sticks to European options so both methods can be compared directly).
Step 1 — simulate the terminal stock price using the closed-form solution to the GBM equation:
S_T = S · exp[ (r − σ²/2) · T + σ · √T · Z ], where Z ~ N(0, 1)
Z is a single draw from a standard normal distribution — this is the
"random" part. Each simulated path draws a fresh Z and produces one
possible future price S_T.
Step 2 — compute the payoff for each simulated path:
Call payoff = max(S_T − K, 0)
Put payoff = max(K − S_T, 0)
Step 3 — average and discount back to present value:
Price ≈ e^(−rT) · (1/N) · Σ payoff_i for i = 1..N
By the Law of Large Numbers, as the number of simulated paths N grows,
this average converges to the true expected discounted payoff — which,
for a European option, is exactly the Black-Scholes price. That
convergence is what the site's chart visualizes: the Monte Carlo estimate
wobbling around the closed-form Black-Scholes line as N gets larger.
Implemented in: backend/services/mc_pricer.py (vectorized with NumPy —
all N paths generated and evaluated in one array operation, no Python
loop) and backend/cpp_src/options.cpp (monte_carlo_option_price(), using
<random>'s Mersenne Twister generator).
The Greeks measure how the option's price changes as each input changes — i.e. they're partial derivatives of the pricing formula. They're what a trading desk actually watches to manage risk, since "the option is worth $4.30" matters less than "the option's value moves $0.60 for every $1 the stock moves."
| Greek | Measures sensitivity of price to... | Formula (call) |
|---|---|---|
| Delta (Δ) | the underlying price S |
N(d1) |
| Gamma (Γ) | the rate of change of Delta itself | φ(d1) / (S·σ·√T) |
| Vega (ν) | volatility σ |
S·φ(d1)·√T |
| Theta (Θ) | time decay, as T shrinks |
−(S·φ(d1)·σ)/(2√T) − r·K·e^(−rT)·N(d2) |
| Rho (ρ) | the risk-free rate r |
K·T·e^(−rT)·N(d2) |
where φ(x) is the standard normal probability density function (not
the cumulative one — it's the "bell curve height" at x, versus N(x)
which is the area under the curve up to x). Put Greeks use the mirrored
formulas (see bs_pricer.py::greeks()).
These are computed analytically (not simulated) since they fall directly out of differentiating the Black-Scholes formula.
Both models start from the same assumption — the underlying follows Geometric Brownian Motion — so they're computing the exact same expectation, just two different ways:
- Black-Scholes solves that expectation's integral in closed form.
- Monte Carlo approximates it by sampling and averaging.
That's why, in the pricing terminal, the Monte Carlo call/put prices land close to (but not exactly on) the Black-Scholes prices, and why the convergence chart's simulated line visibly tightens around the analytical line as the path count increases. Small persistent gaps at low path counts are simulation noise, not model disagreement.
Request comes in (S, K, r, σ, T, path count)
│
▼
┌───────────────────────────┐
│ services/cpp_wrapper.py │ picks an engine: cpp → python fallback
└───────────┬───────────────┘
│
┌────────┴─────────┐
▼ ▼
cpp_src/options.cpp services/bs_pricer.py + services/mc_pricer.py
(compiled binary, (SciPy norm.cdf for Black-Scholes,
called via NumPy vectorized Monte Carlo,
subprocess) Greeks via analytical derivatives)
│ │
└─────────┬─────────┘
▼
routers/pricing.py assembles the response:
BS price, MC price, Greeks, convergence series, engine used, timing
▼
frontend/script.js renders the terminal output + convergence canvas
If neither the compiled binary nor a live backend is reachable at all (e.g.
you open index.html directly as a file), frontend/script.js includes a
third, self-contained implementation of the same formulas in plain
JavaScript, so the page still works standalone.
option-pricing-render/
├── frontend/
│ ├── index.html Marketing site + interactive pricing terminal
│ ├── style.css Design system (dark, terminal-inspired)
│ └── script.js Terminal logic + offline JS fallback pricer
│
├── backend/
│ ├── app.py FastAPI app, mounts the frontend + API
│ ├── routers/pricing.py /api/pricing/* endpoints
│ ├── services/
│ │ ├── bs_pricer.py Black-Scholes + Greeks (SciPy)
│ │ ├── mc_pricer.py Vectorized NumPy Monte Carlo + convergence
│ │ └── cpp_wrapper.py Calls the compiled C++ binary, falls back to Python
│ ├── schemas/pricing.py Pydantic request/response models
│ ├── cpp_src/options.cpp C++ implementation (CLI-friendly)
│ ├── compile_cpp.py Compiles options.cpp -> options_bin
│ └── requirements.txt
│
├── render.yaml Render Blueprint (Docker-based web service)
├── Dockerfile Multi-stage build: compile C++, then Python runtime
├── docker-compose.yml Local dev
├── build.sh / start.sh Alternative native (non-Docker) Render deploy
├── .env.example
└── .dockerignore
bash
cd option-pricing-render
docker compose up --buildPowerShell
cd option-pricing-render
docker compose up --buildThen open http://localhost:8000. Stop with Ctrl+C, or
docker compose down to remove the container.
bash
cd option-pricing-render/backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python compile_cpp.py # optional — needs g++/clang++; falls back to Python if missing
uvicorn app:app --reload --port 8000PowerShell
cd option-pricing-render\backend
python -m venv venv
.\venv\Scripts\Activate.ps1
pip install -r requirements.txt
python compile_cpp.py # optional — needs a C++ compiler on PATH; falls back to Python if missing
uvicorn app:app --reload --port 8000Then open http://localhost:8000.
Returns engine status:
{ "status": "ok", "cpp_engine_available": true }Request body:
{
"spot": 100,
"strike": 100,
"rate": 0.05,
"volatility": 0.2,
"maturity": 1.0,
"simulations": 100000,
"engine": "auto"
}engine:"auto"prefers the compiled C++ path and falls back to Python silently;"cpp"forces C++ and errors if it isn't compiled;"python"forces the NumPy/SciPy path.
Response: Black-Scholes call/put prices, Monte Carlo call/put prices, Greeks for both, a convergence series (MC estimate at increasing path counts), which engine actually executed the request, and compute time in milliseconds.
Render, Docker (recommended, matches render.yaml):
- Push this repo to GitHub.
- In Render: New + → Blueprint → point at the repo.
Render, native Python:
- New + → Web Service
- Build command:
bash build.sh - Start command:
bash start.sh
See Dockerfile for the exact multi-stage build (compiles the C++ kernel
in one stage, copies the binary into a slim Python runtime in the next).
MIT — use it, learn from it, adapt it.