The Gap Between Theory and Practice
When I started building my Monte Carlo options pricing engine, I wanted to bridge the gap between what I learnt in textbooks and what actually happens on a trading desk. The breakthrough came when I discovered the concept of P&L attribution.
Here’s the problem: your model says you should have made $100 today based on your Greeks. But you actually made $115. Where did that extra $15 come from? Is your model wrong? Did correlation break down? Is there unhedged gamma exposure?
This is P&L attribution, and it’s what separates desk quants from academic quants.
View the prject on my GitHub:
What I Built
I created a production-grade Monte Carlo simulation engine with four main components:
1. Core Monte Carlo Engine
At its heart, the engine simulates stock price paths using Geometric Brownian Motion under the risk-neutral measure:
where Z ~ N(0,1) is a standard normal random variable.
I used antithetic variates for variance reduction. For each random path with Z, I also simulate -Z. This simple trick preserves the expected value, introduces negative correlation between paths and reduces variance by approximately 50%.
# Generate random shocksZ = self.rng.standard_normal(n_paths_to_generate)if self.use_antithetic: # Create antithetic variates Z = np.concatenate([Z, -Z])
This means I get the accuracy of 100K simulations using only 50K random samples.
2. Multiple Option Types
The engine prices three types of options:
- European Option: The baseline. I validate these against the Black-Scholes analytical formula. With 100,000 simulations, I consistently get within 0.5% of the analytical price.
- Asian Options: Path-dependent options where payoff depends on the average price over the option’s life. These are interesting because:
- Arithmetic average > Geometric average (Jensen’s inequality)
- Lower price than European (averaging reduces volatility)
- No analytical solution exists (Monte Carlo shines here)
- Barrier Options: Options that knock out if the price crosses a barrier. An up-and-out call with a barrier at $120 trades at an 87% discount to the vanilla European call due to knock-out risk.
3. Greeks Calculation
All five main Greeks calculated via finite differences:
- Delta (Δ): Sensitivity to spot price
- Gamma (Γ): Rate of Delta change
- Vega (ν): Sensitivity to volatility
- Theta (Θ): Time decay
- Rho (ρ): Sensitivity to interest rates
The implementation uses bump-and-revalue:
# Delta: ∂V/∂Soption_up.spot += bump_sizeprice_up = self.price_option(option_up)['price']delta = (price_up - base_price) / bump_size
4. P&L Attribution Framework
This is the key differentiator that shows understanding of real trading operations. The framework decomposes P&L into three parts:
- Clean P&L – Theoretical P&L from Greek sensitivities:
2. Dirty P&L – Actual P&L including execution costs:
3. Residual P&L – The unexplained variance:
The critical rule: If residual P&L > 5% of clean P&L, you have a model failure. Here’s a real example from the code:
CLEAN P&L (Theoretical)────────────────────────────────────────────────────────── Delta P&L: $ 134.44 Gamma P&L: $ 11.18 Vega P&L: $ 42.75 Theta P&L: $ -6.57 Rho P&L: $ 0.00 ──────────────────────────────────────────────────────── TOTAL CLEAN: $ 181.80DIRTY P&L (Actual)────────────────────────────────────────────────────────── Actual P&L: $ 18,191.63 Execution: -$ 20.00 ──────────────────────────────────────────────────────── TOTAL DIRTY: $ 18,171.63RESIDUAL P&L (Unexplained)────────────────────────────────────────────────────────── Residual: $ 17,989.83 Percentage: 9895.48% Status: SIGNIFICANT MODEL FAILUREWARNING: High residual P&L - potential model failure or unhedged risk!
This massive residual screams model failure. Potential causes:
- Correlation breakdown between assets
- Jump event not captured by GBM
- Volatility smile effects
- Unhedged gamma in a fast market
- Model parameters haven’t been updated
Validation & Testing
I built a comprehensive test suite (19 tests) that validates:
Monte Carlo convergence – Prices converge to Black-Scholes with increasing simulations
Put-Call parity – holds within 1%
Greeks validity – Delta in [0,1] for calls, Gamma > 0, Vega > 0, Theta < 0
Asian inequality – Arithmetic average ≥ Geometric average
Barrier discount – Barrier options cheaper than vanilla
$ pytest tests/ -v19 passed in 10.16s
Every mathematical property is verified programmatically.
Key Technical Decisions
Why Monte Carlo over Binomial Trees?
Trees are great for simple options, but Monte Carlo:
- Scales linearly with number of assets (trees scale exponentially)
- Handles path-dependent payoffs naturally
- Easier to implement complex stochastic processes
- Provides standard error estimates
Why NumPy over Loops?
Vectorization makes a massive difference. Compare:
# Loop (slow)for i in range(n_paths): paths[i] = spot * np.exp(drift + diffusion * Z[i])# Vectorized (10 times faster)paths = spot * np.exp(drift + diffusion * Z)
With 100K simulations, the vectorized version runs in ~150ms vs ~1.5s for loops.
Design Pattern: Separate Concerns
I used class inheritance for options:
class BaseOption(ABC): abstractmethod def payoff(self, paths): pass property abstractmethod def is_path_dependent(self) -> bool: pass
This makes it trivial to add new option types: just implement the payoff() method.
Results
European Call (K=100, S=100, T=1, σ=0.2, r=0.05):
| Method | Price | Error | Time |
|---|---|---|---|
| Black-Scholes | $10.4506 | – | – |
| Monte Carlo (100K) | $10.4673 | 0.16% | 0.15s |
| MC + Antithetic | $10.4511 | 0.005% | 0.15s |
The antithetic variance reduction improves accuracy by ~32x with zero performance cost.
Try It Yourself
The complete code is open source on GitHub:
There you will find quick start instructions.
Final Thoughts
Building this project taught me that correctness matters more than complexity. A simple Monte Carlo engine with proper validation beats a complex model with no tests.
The P&L attribution framework was the breakthrough. It transformed the project from another pricing implementation to learning more about and demonstrating more real desk operations.
Understanding when your model is wrong matters more than having a fancy model. More on this in a later post on my master thesis on option pricing.
I hope you find some of this interesting. I surely do!






Leave a Reply