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:

ST=S0∗exp((r−0.5σ2)T+σ√T·Z)S_T = S_0 * exp((r – 0.5σ²)T + σ√T·Z)

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 shocks
Z = 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/∂S
option_up.spot += bump_size
price_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:

  1. Clean P&L – Theoretical P&L from Greek sensitivities:
CleanP&L=Δ·ΔS+½Γ·(ΔS)2+ν·Δσ+Θ·Δt+ρ·ΔrClean P\&L = Δ·ΔS + ½Γ·(ΔS)² + ν·Δσ + Θ·Δt + ρ·Δr

2. Dirty P&L – Actual P&L including execution costs:

DirtyP&L=ActualP&L−(bid−askspread+slippage+funding+transactioncosts)Dirty P\&L = Actual P\&L – (bid-ask spread + slippage + funding + transaction costs)

3. Residual P&L – The unexplained variance:

ResidualP&L=DirtyP&L−CleanP&LResidual P\&L = Dirty P\&L – Clean P\&L

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.80
DIRTY P&L (Actual)
──────────────────────────────────────────────────────────
Actual P&L: $ 18,191.63
Execution: -$ 20.00
────────────────────────────────────────────────────────
TOTAL DIRTY: $ 18,171.63
RESIDUAL P&L (Unexplained)
──────────────────────────────────────────────────────────
Residual: $ 17,989.83
Percentage: 9895.48%
Status: SIGNIFICANT MODEL FAILURE
WARNING: 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 – C−P=S−Ke−rT C – P = S – Ke^{-rT} 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/ -v
19 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):

MethodPriceErrorTime
Black-Scholes$10.4506––
Monte Carlo (100K)$10.46730.16%0.15s
MC + Antithetic$10.45110.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

Trending

Discover more from Convergence Point

Subscribe now to keep reading and get access to the full archive.

Continue reading