Monte Carlo Simulation for Portfolio Risk
Monte Carlo simulation is a powerful technique used by quant funds and risk managers to model uncertainty. Instead of predicting a single outcome, you simulate thousands of possible futures based on historical statistics. In this tutorial, we build a Monte Carlo simulator that projects portfolio value over time and calculates Value at Risk (VaR).
Prerequisites
- ✓Basic Python and NumPy
- ✓Understanding of normal distributions
- ✓Basic portfolio theory
Step 1.Install dependencies
NumPy for random number generation, yfinance for historical returns.
pip install numpy pandas matplotlib yfinanceStep 2.Get historical returns
We extract mean daily returns and the covariance matrix, which capture both expected returns and how assets move together.
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
tickers = ["AAPL", "MSFT", "GOOGL"]
data = yf.download(tickers, start="2019-01-01", end="2024-01-01")["Close"]
returns = data.pct_change().dropna()
mean_returns = returns.mean()
cov_matrix = returns.cov()
print("Annualised returns:")
print((mean_returns * 252).round(4))Step 3.Set up the simulation
We define a weighted portfolio and calculate its expected daily return and volatility.
NUM_SIMULATIONS = 10_000
NUM_DAYS = 252 # 1 year
INITIAL_VALUE = 100_000
WEIGHTS = np.array([0.4, 0.35, 0.25])
portfolio_mean = np.sum(mean_returns * WEIGHTS)
portfolio_std = np.sqrt(np.dot(WEIGHTS.T, np.dot(cov_matrix, WEIGHTS)))
print(f"Portfolio daily mean: {portfolio_mean:.6f}")
print(f"Portfolio daily std: {portfolio_std:.6f}")Step 4.Run Monte Carlo simulation
Each simulation generates a random path of daily returns drawn from the portfolio distribution.
np.random.seed(42)
all_paths = np.zeros((NUM_SIMULATIONS, NUM_DAYS))
for i in range(NUM_SIMULATIONS):
daily_returns = np.random.normal(portfolio_mean, portfolio_std, NUM_DAYS)
price_path = INITIAL_VALUE * np.cumprod(1 + daily_returns)
all_paths[i] = price_path
final_values = all_paths[:, -1]
print(f"Mean final value: ${np.mean(final_values):,.0f}")
print(f"Median final value: ${np.median(final_values):,.0f}")
print(f"Best case (95th): ${np.percentile(final_values, 95):,.0f}")
print(f"Worst case (5th): ${np.percentile(final_values, 5):,.0f}")Step 5.Calculate Value at Risk
VaR tells you the maximum expected loss at a given confidence level. 95% VaR means there is a 5% chance of losing more than this amount.
var_95 = INITIAL_VALUE - np.percentile(final_values, 5)
var_99 = INITIAL_VALUE - np.percentile(final_values, 1)
print(f"1-Year 95% VaR: ${var_95:,.0f}")
print(f"1-Year 99% VaR: ${var_99:,.0f}")
print(f"Probability of loss: {(final_values < INITIAL_VALUE).mean() * 100:.1f}%")Step 6.Visualise simulation paths
Each line represents one possible future. The spread shows the range of outcomes.
plt.figure(figsize=(14, 8))
for i in range(min(200, NUM_SIMULATIONS)):
plt.plot(all_paths[i], alpha=0.05, color="cyan")
plt.axhline(INITIAL_VALUE, color="white", linestyle="--", alpha=0.5)
plt.xlabel("Trading Days")
plt.ylabel("Portfolio Value ($)")
plt.title(f"Monte Carlo Simulation ({NUM_SIMULATIONS:,} paths)")
plt.savefig("monte_carlo.png", dpi=150)
plt.show()Expected Output
Mean final value: $128,450
Median final value: $125,200
1-Year 95% VaR: $24,300
1-Year 99% VaR: $35,800
Probability of loss: 22.4%Next Steps
- →Add rebalancing to the simulation
- →Compare VaR across different portfolio allocations
- →Implement Conditional VaR (CVaR / Expected Shortfall)
Recommended Reading
Monte Carlo Methods in Financial Engineering →As an Amazon Associate we may earn from qualifying purchases.