CodeForFinance
Python

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 yfinance

Step 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.

Browse All Tutorials →

Developer Essentials

As an Amazon Associate we may earn from qualifying purchases.