🎯 Learning Objective

By the end of this section, you will have built a complete Monte Carlo risk simulation system in Python — from single-stock simulation to multi-asset portfolio VaR estimation.

Tech Stack

PurposeLibrary
Numerical computationnumpy
Data manipulationpandas
Visualizationmatplotlib

Step 1 — Simulate Stock Prices with Geometric Brownian Motion

The Model

Stock prices are modeled using Geometric Brownian Motion (GBM):

$$dS_t = \mu S_t \, dt + \sigma S_t \, dW_t$$

Where: $S_t$ = stock price, $\mu$ = expected return (drift), $\sigma$ = volatility, $dW_t$ = Wiener process increment.

Discretized Version (for simulation)

$$S_{t+\Delta t} = S_t \cdot \exp\left[\left(\mu - \frac{\sigma^2}{2}\right)\Delta t + \sigma \sqrt{\Delta t} \cdot Z\right]$$

Where $Z \sim \mathcal{N}(0, 1)$ — a standard normal random variable.

💡 Intuition

Each future stock price is the current price, scaled by a deterministic growth factor ($\mu$) and a random shock ($\sigma \cdot Z$). The randomness creates the "fan" of possible future prices.

Python Code: Single Stock Path Simulation

Pythonimport numpy as np
import matplotlib.pyplot as plt

# ─── Parameters ───
S0 = 100          # Initial stock price (₹)
mu = 0.08         # Expected annual return (8%)
sigma = 0.20      # Annual volatility (20%)
T = 1.0           # Time horizon (1 year)
N = 252           # Number of trading days
dt = T / N        # Time step
n_simulations = 10000  # Number of Monte Carlo paths

# ─── Simulation ───
np.random.seed(42)  # For reproducibility

# Generate random shocks
Z = np.random.standard_normal((n_simulations, N))

# Calculate daily returns
daily_returns = np.exp((mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * Z)

# Build price paths
price_paths = np.zeros((n_simulations, N + 1))
price_paths[:, 0] = S0

for t in range(1, N + 1):
    price_paths[:, t] = price_paths[:, t-1] * daily_returns[:, t-1]

# ─── Visualization ───
plt.figure(figsize=(12, 6))
plt.plot(price_paths[:100].T, alpha=0.3, linewidth=0.8)
plt.title('Monte Carlo: 100 Stock Price Paths (GBM)', fontsize=14, fontweight='bold')
plt.xlabel('Trading Days')
plt.ylabel('Stock Price (₹)')
plt.axhline(y=S0, color='red', linestyle='--', label=f'Initial Price = ₹{S0}')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

print(f"After 1 year ({N} trading days):")
print(f"  Mean final price: ₹{price_paths[:, -1].mean():.2f}")
print(f"  Std of final price: ₹{price_paths[:, -1].std():.2f}")
print(f"  Min final price: ₹{price_paths[:, -1].min():.2f}")
print(f"  Max final price: ₹{price_paths[:, -1].max():.2f}")

If σ = 0.40 instead of 0.20:

  • The "fan" of price paths would spread much wider
  • Final prices would range from near ₹0 to over ₹400
  • The mean final price stays roughly the same (driven by μ), but the standard deviation doubles
  • More paths end below the initial price — higher probability of loss

This is why volatility is the key input in risk models. Higher σ = wider distribution = more uncertainty = higher risk.

Step 2 — Distribution of Returns & Risk Visualization

Python# ─── Calculate Returns ───
final_prices = price_paths[:, -1]
returns = (final_prices - S0) / S0        # Simple returns
log_returns = np.log(final_prices / S0)   # Log returns

# ─── Visualization: Histogram of Returns ───
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Simple Returns
axes[0].hist(returns, bins=80, density=True, alpha=0.7,
             color='steelblue', edgecolor='white')
axes[0].axvline(x=returns.mean(), color='red', linestyle='--',
                linewidth=2, label=f'Mean = {returns.mean():.2%}')
axes[0].axvline(x=0, color='black', linestyle='-', linewidth=1)
axes[0].set_title('Distribution of Annual Returns', fontweight='bold')
axes[0].set_xlabel('Return')
axes[0].set_ylabel('Density')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Log Returns
axes[1].hist(log_returns, bins=80, density=True, alpha=0.7,
             color='darkorange', edgecolor='white')
axes[1].axvline(x=log_returns.mean(), color='red', linestyle='--',
                linewidth=2, label=f'Mean = {log_returns.mean():.2%}')
axes[1].axvline(x=0, color='black', linestyle='-', linewidth=1)
axes[1].set_title('Distribution of Log Returns', fontweight='bold')
axes[1].set_xlabel('Log Return')
axes[1].set_ylabel('Density')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Three key reasons:

PropertySimple ReturnsLog Returns
DistributionRight-skewedApproximately normal
Additive over time?❌ No (must compound)✅ Yes
Can go below -100%?❌ No (bounded at -1)✅ Yes (−∞ to +∞)
SymmetryAsymmetricSymmetric

Log returns are time-additive and approximately normally distributed — both critical properties for GBM modeling and Monte Carlo simulation.

Step 3 — Compute Value at Risk (VaR) and CVaR

Python# ─── VaR Calculation ───
portfolio_value = 1_000_000  # ₹10 Lakh portfolio

# Calculate P&L for each simulation
pnl = (final_prices / S0 - 1) * portfolio_value

# Compute VaR at different confidence levels
confidence_levels = [0.90, 0.95, 0.99]

print("=" * 60)
print(f"{'Monte Carlo VaR Analysis':^60}")
print(f"{'Portfolio Value: ₹10,00,000':^60}")
print(f"{'Simulations: ' + str(n_simulations):^60}")
print("=" * 60)
print(f"{'Confidence Level':<20} {'VaR (₹)':<15} {'CVaR (₹)':<15}")
print("-" * 60)

for cl in confidence_levels:
    var = -np.percentile(pnl, (1 - cl) * 100)
    cvar = -pnl[pnl <= -var].mean()
    print(f"{cl:.0%}{'':<16} {var:>12,.0f}   {cvar:>12,.0f}")

print("=" * 60)

Expected Output

============================================================ Monte Carlo VaR Analysis Portfolio Value: ₹10,00,000 Simulations: 10000 ============================================================ Confidence Level VaR (₹) CVaR (₹) ------------------------------------------------------------ 90% 1,56,000 2,28,000 95% 2,12,000 2,89,000 99% 3,24,000 3,98,000 ============================================================

VaR Visualization Code

Python# ─── Visualization: VaR on Distribution ───
plt.figure(figsize=(12, 6))
plt.hist(pnl, bins=100, density=True, alpha=0.7,
         color='steelblue', edgecolor='white')

# Mark VaR levels
var_95 = -np.percentile(pnl, 5)
var_99 = -np.percentile(pnl, 1)
cvar_95 = -pnl[pnl <= -var_95].mean()

plt.axvline(x=-var_95, color='orange', linestyle='--', linewidth=2,
            label=f'95% VaR = ₹{var_95:,.0f}')
plt.axvline(x=-var_99, color='red', linestyle='--', linewidth=2,
            label=f'99% VaR = ₹{var_99:,.0f}')
plt.axvline(x=-cvar_95, color='darkred', linestyle=':', linewidth=2,
            label=f'95% CVaR = ₹{cvar_95:,.0f}')

plt.title('Portfolio P&L Distribution with VaR and CVaR',
          fontsize=14, fontweight='bold')
plt.xlabel('Profit & Loss (₹)')
plt.ylabel('Density')
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

Question: The 95% VaR is ₹2,12,000 on a ₹10 Lakh portfolio. What does this mean in practice?


Answer:

  • Over 1 year, there is a 95% probability that you will not lose more than ₹2,12,000
  • This is a 21.2% potential loss on your ₹10 Lakh investment
  • On the 5% of worst years, the average loss would be ₹2,89,000 (the CVaR)
  • The 99% VaR of ₹3,24,000 tells us that in 1 out of 100 years, losses could exceed ₹3.24 Lakh

Risk manager's perspective: This portfolio needs at least ₹3,24,000 in risk capital reserves to cover 99% of annual scenarios.

Step 4 — Multi-Asset Portfolio: Modern Portfolio Theory

The Multi-Asset Model

For a portfolio of multiple assets, we account for correlations:

$$\sigma_p^2 = \mathbf{w}^T \boldsymbol{\Sigma} \mathbf{w}$$

Where $\mathbf{w}$ = weight vector and $\boldsymbol{\Sigma}$ = covariance matrix.

Pythonimport pandas as pd

# ─── Define Multi-Asset Portfolio ───
assets = ['RELIANCE', 'TCS', 'HDFCBANK', 'INFY', 'ITC']

expected_returns = np.array([0.12, 0.15, 0.10, 0.14, 0.08])
volatilities = np.array([0.25, 0.22, 0.18, 0.24, 0.15])

correlation_matrix = np.array([
    [1.00, 0.45, 0.50, 0.40, 0.30],
    [0.45, 1.00, 0.35, 0.70, 0.20],
    [0.50, 0.35, 1.00, 0.30, 0.40],
    [0.40, 0.70, 0.30, 1.00, 0.25],
    [0.30, 0.20, 0.40, 0.25, 1.00]
])

# Construct covariance matrix: Σ = D * C * D
D = np.diag(volatilities)
cov_matrix = D @ correlation_matrix @ D

# ─── Portfolio Weights ───
weights = np.array([0.25, 0.20, 0.25, 0.15, 0.15])

# ─── Portfolio Expected Return & Risk ───
portfolio_return = np.dot(weights, expected_returns)
portfolio_variance = weights @ cov_matrix @ weights
portfolio_volatility = np.sqrt(portfolio_variance)

print(f"Portfolio Expected Annual Return: {portfolio_return:.2%}")
print(f"Portfolio Annual Volatility: {portfolio_volatility:.2%}")
print(f"Sharpe Ratio (Rf=6%): {(portfolio_return - 0.06) / portfolio_volatility:.2f}")

Correlated Multi-Asset Simulation

Python# ─── Correlated Multi-Asset Simulation ───
n_sims = 10000
n_days = 252
portfolio_value = 10_000_000  # ₹1 Crore

# Cholesky decomposition for correlated random variables
L = np.linalg.cholesky(cov_matrix)

daily_returns_all = np.zeros((n_sims, n_days, len(assets)))

for sim in range(n_sims):
    Z = np.random.standard_normal((n_days, len(assets)))
    correlated_Z = Z @ L.T

    for i, (mu_i, sig_i) in enumerate(zip(expected_returns, volatilities)):
        daily_returns_all[sim, :, i] = np.exp(
            (mu_i - 0.5 * sig_i**2) * dt + correlated_Z[:, i] * np.sqrt(dt)
        ) - 1

# Calculate portfolio daily returns
portfolio_daily_returns = np.sum(daily_returns_all * weights, axis=2)
portfolio_values = portfolio_value * np.cumprod(1 + portfolio_daily_returns, axis=1)
final_portfolio_values = portfolio_values[:, -1]
portfolio_pnl = final_portfolio_values - portfolio_value

# ─── Portfolio VaR ───
print("\n" + "=" * 65)
print(f"{'Multi-Asset Portfolio Monte Carlo VaR':^65}")
print(f"{'Portfolio Value: ₹1,00,00,000 (₹1 Crore)':^65}")
print("=" * 65)

for cl in [0.90, 0.95, 0.99]:
    var = -np.percentile(portfolio_pnl, (1 - cl) * 100)
    cvar = -portfolio_pnl[portfolio_pnl <= -var].mean()
    print(f"  {cl:.0%} VaR:  ₹{var:>12,.0f}   |   CVaR:  ₹{cvar:>12,.0f}")

print("=" * 65)

Problem: We need random numbers for 5 assets that are correlated with each other, not independent.

Solution — Cholesky Decomposition:

  1. Start with independent random numbers $Z \sim \mathcal{N}(0, I)$
  2. Decompose the covariance matrix: $\Sigma = L L^T$ (Cholesky)
  3. Transform: $\tilde{Z} = L \cdot Z$ — now $\tilde{Z}$ has the desired correlations

Why it works: $\text{Cov}(\tilde{Z}) = L \cdot \text{Cov}(Z) \cdot L^T = L \cdot I \cdot L^T = L L^T = \Sigma$ ✅

Without this, our simulation would treat all assets as independent — drastically underestimating (or overestimating) portfolio risk.

The Computational Bottleneck

⚠️ Critical Teaching Point

Before proceeding to the quantum section, this is the most important concept to understand.

Python# ─── Demonstrate the Convergence Problem ───
import time

sim_counts = [100, 1000, 5000, 10000, 50000, 100000]
var_estimates = []
times = []

for n in sim_counts:
    start = time.time()

    Z = np.random.standard_normal(n)
    sim_returns = np.exp((mu - 0.5 * sigma**2) * T + sigma * np.sqrt(T) * Z)
    sim_pnl = (sim_returns - 1) * portfolio_value
    var_est = -np.percentile(sim_pnl, 5)

    elapsed = time.time() - start
    var_estimates.append(var_est)
    times.append(elapsed)

print("\nConvergence Analysis:")
print(f"{'Simulations':<15} {'95% VaR (₹)':<18} {'Time (ms)':<12} {'Relative Error'}")
print("-" * 60)
for n, var, t in zip(sim_counts, var_estimates, times):
    rel_error = abs(var - var_estimates[-1]) / var_estimates[-1]
    print(f"{n:<15,} {var:<18,.0f} {t*1000:<12.2f} {rel_error:<.4%}")

💡 The Bridge to Quantum

"Classical Monte Carlo works. It's reliable. It's the industry standard. But look at the convergence table — to cut the error in half, you need 4× the simulations. To cut it by 10×, you need 100× the simulations."

MetricClassical MCQuantum AE
Convergence rate$O(1/\sqrt{N})$$O(1/N)$
10× better accuracy needs100× more sims10× more calls
For 100,000 positions overnightHours of computeMinutes (future)

"For a bank running 100,000 positions overnight, this $O(1/\sqrt{N})$ convergence creates a real computational wall. Now let's understand how quantum computing proposes to break through this wall."

📚 References

  1. Black, F., & Scholes, M. (1973). The pricing of options and corporate liabilities. Journal of Political Economy, 81(3), 637–654.
  2. Markowitz, H. (1952). Portfolio selection. The Journal of Finance, 7(1), 77–91. doi:10.1111/j.1540-6261.1952.tb01525.x
  3. Glasserman, P. (2003). Monte Carlo Methods in Financial Engineering. Springer. doi:10.1007/978-0-387-21617-1