Monte Carlo Portfolio Simulation System Development
Monte Carlo simulation generates thousands of possible future portfolio trajectories based on statistical properties of market data. This allows estimating the distribution of possible outcomes, not just one "expected" value.
Monte Carlo Simulation Principle
Instead of "portfolio will grow X%", we say: "with 70% probability portfolio will grow 20-80%, with 15% probability it will lose 10-30%".
Basic GBM (Geometric Brownian Motion):
import numpy as np
def simulate_gbm(initial_price, mu, sigma, days, n_simulations=10000):
"""
mu: average daily return
sigma: average daily volatility
"""
dt = 1 # 1 day
random_returns = np.random.normal(
mu * dt,
sigma * np.sqrt(dt),
(n_simulations, days)
)
# Cumulative returns
cumulative = np.cumprod(1 + random_returns, axis=1)
price_paths = initial_price * cumulative
return price_paths
Multi-asset Simulation with Correlations
For portfolio it's important to account correlations between assets:
from numpy.linalg import cholesky
def simulate_correlated_portfolio(initial_prices, means, cov_matrix,
days=365, n_sims=10000):
"""
initial_prices: dict {symbol: price}
means: dict {symbol: daily_mean}
cov_matrix: covariance matrix of daily returns
"""
n_assets = len(initial_prices)
symbols = list(initial_prices.keys())
# Cholesky decomposition for generating correlated random numbers
L = cholesky(cov_matrix)
portfolio_paths = []
for _ in range(n_sims):
# Generate uncorrelated normal random numbers
z = np.random.standard_normal((days, n_assets))
# Apply correlation structure
correlated_returns = z @ L.T
# Add drifts (mean returns)
daily_means = np.array([means[s] for s in symbols])
actual_returns = correlated_returns + daily_means
# Price evolution
prices = np.zeros((days + 1, n_assets))
prices[0] = [initial_prices[s] for s in symbols]
for t in range(1, days + 1):
prices[t] = prices[t-1] * (1 + actual_returns[t-1])
# Portfolio value (assume equal weights)
weights = np.ones(n_assets) / n_assets
portfolio_value = (prices * weights).sum(axis=1)
portfolio_paths.append(portfolio_value)
return np.array(portfolio_paths)
Enhanced Return Models
GBM assumes normal distribution of returns. For crypto this is incorrect — there are fat tails and volatility clustering.
Student's t-distribution for fat tails:
from scipy.stats import t as t_dist
def simulate_fat_tail(mu, sigma, df, n_sims, days):
returns = t_dist.rvs(df=df, loc=mu, scale=sigma, size=(n_sims, days))
return np.cumprod(1 + returns, axis=1)
GARCH(1,1) conditional volatility: tomorrow's volatility depends on today's return and yesterday's volatility.
from arch import arch_model
def fit_garch_and_simulate(returns_history, n_sims=10000, horizon=252):
model = arch_model(returns_history * 100, vol='GARCH', p=1, q=1)
result = model.fit(disp='off')
# Simulate from fitted GARCH model
simulations = result.forecast(horizon=horizon, method='simulation',
simulations=n_sims)
return simulations.simulations.values
Analysis of Simulation Results
def analyze_simulation_results(portfolio_paths, initial_value,
confidence_levels=[0.05, 0.25, 0.50, 0.75, 0.95]):
final_values = portfolio_paths[:, -1]
# Distribution of final values
percentiles = {f'p{int(c*100)}': np.percentile(final_values, c*100)
for c in confidence_levels}
# Probability of loss
prob_loss = (final_values < initial_value).mean()
# Expected return and std
returns = (final_values - initial_value) / initial_value
# VaR from simulation
var_95 = np.percentile(final_values - initial_value, 5)
cvar_95 = (final_values - initial_value)[
final_values - initial_value <= var_95
].mean()
# Max drawdown distribution
max_drawdowns = []
for path in portfolio_paths:
peaks = np.maximum.accumulate(path)
drawdowns = (peaks - path) / peaks
max_drawdowns.append(drawdowns.max())
return {
'percentiles': percentiles,
'prob_loss': prob_loss,
'expected_return': returns.mean(),
'return_std': returns.std(),
'var_95': var_95,
'cvar_95': cvar_95,
'avg_max_drawdown': np.mean(max_drawdowns),
'worst_max_drawdown': np.max(max_drawdowns)
}
Result Visualization
Fan chart: shows the range of possible portfolio trajectories. Central line is median (50th percentile). Darker zones are probable ranges (25-75%), lighter are rare (5-95%).
Return distribution histogram: histogram of final returns from all simulations. Normal vs actual distribution.
Drawdown distribution: histogram of maximum drawdowns across simulations. Shows probability of various drawdown levels.
Applications in Portfolio Management
Probability of achieving goal: "what's the probability portfolio will grow 50% in a year with current strategy?"
Strategy comparison: run simulation for two strategies, compare outcome distributions.
Optimal rebalancing frequency: simulate portfolio with different rebalancing frequencies, select optimal.
Capital allocation: how much to allocate to risky vs conservative strategy to achieve target with acceptable risk?
Technical Stack
Python (numpy, scipy, arch for GARCH), Numba for simulation acceleration (JIT compilation gives 10-50x speedup), pandas for data processing, matplotlib/plotly for fan chart visualization. 10,000 simulations for 252 days take < 1 second with Numba.
Develop Monte Carlo simulation system with support for GBM, fat-tail distributions and GARCH model, multi-asset simulation with correlations, result analysis (percentiles, VaR, drawdown distribution) and interactive visualizations.







