Розробка системи Monte Carlo симуляції портфеля
Monte Carlo симуляція генерує тисячі можливих майбутніх траєкторій портфеля на основі статистичних властивостей ринкових даних. Це дозволяє оцінити розподіл можливих результатів, а не просто одне «очікуване» значення.
Принцип Monte Carlo симуляції
Замість «портфель виросте на X%» говоримо: «з ймовірністю 70% портфель виросте на 20-80%, з ймовірністю 15% втратить 10-30%».
Базова GBM (Geometric Brownian Motion):
import numpy as np
def simulate_gbm(initial_price, mu, sigma, days, n_simulations=10000):
"""
mu: середня денна дохідність
sigma: середня денна волатильність
"""
dt = 1 # 1 день
random_returns = np.random.normal(
mu * dt,
sigma * np.sqrt(dt),
(n_simulations, days)
)
# Кумулятивні returns
cumulative = np.cumprod(1 + random_returns, axis=1)
price_paths = initial_price * cumulative
return price_paths
Мультиактивна симуляція з кореляціями
Для портфеля важливо враховувати кореляції між активами:
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: матриця коваріацій денних returns
"""
n_assets = len(initial_prices)
symbols = list(initial_prices.keys())
# Розкладання Холецького для генерації корельованих випадкових чисел
L = cholesky(cov_matrix)
portfolio_paths = []
for _ in range(n_sims):
# Генеруємо некорельовані нормальні випадкові числа
z = np.random.standard_normal((days, n_assets))
# Застосовуємо кореляційну структуру
correlated_returns = z @ L.T
# Додаємо drifts (mean returns)
daily_means = np.array([means[s] for s in symbols])
actual_returns = correlated_returns + daily_means
# Еволюція цін
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])
# Вартість портфеля (припускаємо рівні ваги)
weights = np.ones(n_assets) / n_assets
portfolio_value = (prices * weights).sum(axis=1)
portfolio_paths.append(portfolio_value)
return np.array(portfolio_paths)
Удосконалені моделі returns
GBM припускає нормальний розподіл returns. Для крипто це неправильно — є fat tails і кластеризація волатильності.
Розподіл Student's t для 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) умовна волатильність: волатильність завтра залежить від дохідності сьогодні та волатильності вчора.
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')
# Симулюємо з налаштованої GARCH моделі
simulations = result.forecast(horizon=horizon, method='simulation',
simulations=n_sims)
return simulations.simulations.values
Аналіз результатів симуляції
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]
# Розподіл фінальних значень
percentiles = {f'p{int(c*100)}': np.percentile(final_values, c*100)
for c in confidence_levels}
# Ймовірність втрати
prob_loss = (final_values < initial_value).mean()
# Очікуваний return і std
returns = (final_values - initial_value) / initial_value
# VaR з симуляції
var_95 = np.percentile(final_values - initial_value, 5)
cvar_95 = (final_values - initial_value)[
final_values - initial_value <= var_95
].mean()
# Розподіл max drawdown
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)
}
Візуалізація результатів
Fan chart: показує діапазон можливих траєкторій портфеля. Центральна лінія — медіана (50-й перцентиль). Темніші зони — вірогідні діапазони (25-75%), світліші — рідкісні (5-95%).
Гістограма розподілу returns: гістограма фінальних returns з усіх симуляцій. Нормальний vs реальний розподіл.
Розподіл drawdown: гістограма максимальних drawdown по симуляціях. Показує ймовірність різних рівнів просідання.
Застосування в управлінні портфелем
Ймовірність досягнення цілі: «яка ймовірність що портфель виросте на 50% за рік при поточній стратегії?»
Порівняння стратегій: запустити симуляцію для двох стратегій, порівняти розподіли результатів.
Оптимальна частота ребалансування: симулювати портфель з різними частотами ребалансування, вибрати оптимальну.
Розподіл капіталу: скільки виділити на ризиковану vs консервативну стратегію для досягнення цілі при допустимому ризику?
Технічний стек
Python (numpy, scipy, arch для GARCH), Numba для прискорення симуляцій (JIT компіляція дає 10-50x прискорення), pandas для обробки даних, matplotlib/plotly для візуалізації fan chart. 10,000 симуляцій на 252 дні займають < 1 секунди з Numba.
Розробляємо систему Monte Carlo симуляції з підтримкою GBM, fat-tail розподілів та GARCH моделі, мультиактивною симуляцією з кореляціями, аналізом результатів (перцентилі, VaR, розподіл drawdown) та інтерактивними візуалізаціями.







