Портфельная теория Марковица для криптовалют: от нуля до героя

Создаем оптимальные крипто-портфели с помощью Python - потому что YOLO это не стратегия. Учимся применять теорию портфелей лауреата Нобелевской премии к криптовалютным инвестициям с практическими примерами кода на Python.

Создаем оптимальные крипто-портфели с помощью Python - потому что YOLO это не стратегия

Портфельная теория Марковица Портфельная теория Марковица: Математическая оптимизация, применяемая к цифровым активам для максимизации доходности при заданном уровне риска.


Введение: Почему вашему крипто-портфелю нужна математика (а не только вайбы)

Привет, крипто-дегены! 👋

Помните тот раз, когда вы вложили всю свою стопку в DOGE из-за твита Илона? Или когда вы в панике распродали всё во время последнего краха? Да, мы все там были. Сегодня мы поговорим о том, что может спасти ваш портфель (и ваше психическое здоровье): портфельная теория Марковица.

Гарри Марковиц буквально получил Нобелевскую премию за эту штуку в 1990 году. Основная идея? Вы можете математически оптимизировать свой портфель, чтобы получить наилучшую возможную доходность для любого заданного уровня риска. Это как иметь GPS для ваших инвестиций вместо вождения с завязанными глазами.

Основная концепция: Риск против доходности (вечный танец)

Прежде чем мы погрузимся в код, давайте поймем, с чем мы имеем дело:

  • Ожидаемая доходность: Сколько денег вы ожидаете заработать
  • Риск (волатильность): Насколько сильно колеблется стоимость вашего портфеля
  • Корреляция: Насколько похоже движутся разные активы

Магия происходит, когда вы комбинируете активы, которые не движутся в идеальной синхронизации. Когда биткоин падает, возможно, некоторые DeFi-токены держатся лучше. Это диверсификация работает на вас.

Риск против доходности Риск против доходности: Балансировка волатильных высокодоходных активов со стабильным фундаментом для достижения оптимальной геометрической средней доходности.

Настройка нашей Python-среды

Прежде всего - подготовим наши инструменты:

# Основные библиотеки для оптимизации крипто-портфеля
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.optimize import minimize
import yfinance as yf
import warnings
warnings.filterwarnings('ignore')

# Настройка стиля графиков
plt.style.use('dark_background')
sns.set_palette("husl")

Уровень 1: Детские шаги - простая портфельная математика

Начнем с основ. Мы рассчитаем доходность и риск для простого портфеля из 2 активов.

def get_crypto_data(symbols, period="1y"):
    """
    Получение данных криптовалют с Yahoo Finance
    symbols: список символов криптовалют (например, ['BTC-USD', 'ETH-USD'])
    period: временной период для данных
    """
    data = yf.download(symbols, period=period)['Adj Close']
    return data

# Получаем данные для Bitcoin и Ethereum
crypto_symbols = ['BTC-USD', 'ETH-USD']
prices = get_crypto_data(crypto_symbols)

# Рассчитываем дневные доходности
returns = prices.pct_change().dropna()
print("Предварительный просмотр дневных доходностей:")
print(returns.head())

Теперь давайте рассчитаем некоторые базовые метрики портфеля:

def portfolio_performance(weights, returns):
    """
    Расчет доходности и волатильности портфеля
    weights: массив весов портфеля
    returns: датафрейм доходностей активов
    """
    # Доходность портфеля (аннуализированная)
    portfolio_return = np.sum(returns.mean() * weights) * 252

    # Волатильность портфеля (аннуализированная)
    portfolio_vol = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))

    return portfolio_return, portfolio_vol

# Пример: 50% BTC, 50% ETH
weights_5050 = np.array([0.5, 0.5])
ret_5050, vol_5050 = portfolio_performance(weights_5050, returns)

print(f"Портфель 50/50:")
print(f"Ожидаемая годовая доходность: {ret_5050:.2%}")
print(f"Годовая волатильность: {vol_5050:.2%}")
print(f"Коэффициент Шарпа: {ret_5050/vol_5050:.3f}")

Уровень 2: Серьезный подход - граница эффективности

Теперь мы готовим! Граница эффективности показывает нам все возможные оптимальные портфели. Каждая точка представляет лучшую возможную доходность для заданного уровня риска.

def generate_random_portfolios(returns, num_portfolios=10000):
    """
    Генерация случайных комбинаций портфелей
    """
    num_assets = len(returns.columns)
    results = np.zeros((4, num_portfolios))

    for i in range(num_portfolios):
        # Генерируем случайные веса
        weights = np.random.random(num_assets)
        weights /= np.sum(weights)  # Нормализуем к сумме 1

        # Рассчитываем метрики портфеля
        portfolio_return, portfolio_vol = portfolio_performance(weights, returns)
        sharpe_ratio = portfolio_return / portfolio_vol

        # Сохраняем результаты
        results[0,i] = portfolio_return
        results[1,i] = portfolio_vol
        results[2,i] = sharpe_ratio
        results[3,i:] = weights

    return results

# Генерируем 10,000 случайных портфелей
results = generate_random_portfolios(returns)

# Создаем датафрейм для удобной обработки
portfolio_results = pd.DataFrame({
    'Returns': results[0],
    'Volatility': results[1],
    'Sharpe_Ratio': results[2]
})

# Строим график границы эффективности
plt.figure(figsize=(12, 8))
scatter = plt.scatter(portfolio_results['Volatility'],
                     portfolio_results['Returns'],
                     c=portfolio_results['Sharpe_Ratio'],
                     cmap='viridis', alpha=0.6)
plt.colorbar(scatter, label='Коэффициент Шарпа')
plt.xlabel('Волатильность (Риск)')
plt.ylabel('Ожидаемая доходность')
plt.title('Граница эффективности - случайные портфели')
plt.show()

Граница эффективности Граница эффективности: Кривая, представляющая максимальную возможную ожидаемую доходность для заданного уровня риска.

Уровень 3: Мастер оптимизации - поиск идеального портфеля

Случайная выборка - это весело, но мы хотим математически оптимальное решение. Время достать тяжелую артиллерию - scipy optimization!

def negative_sharpe_ratio(weights, returns, risk_free_rate=0.02):
    """
    Расчет отрицательного коэффициента Шарпа (мы минимизируем это)
    """
    portfolio_return, portfolio_vol = portfolio_performance(weights, returns)
    sharpe = (portfolio_return - risk_free_rate) / portfolio_vol
    return -sharpe

def minimize_volatility(weights, returns):
    """
    Расчет волатильности портфеля (мы минимизируем это)
    """
    _, portfolio_vol = portfolio_performance(weights, returns)
    return portfolio_vol

def portfolio_return_objective(weights, returns):
    """
    Расчет доходности портфеля (мы максимизируем это)
    """
    portfolio_return, _ = portfolio_performance(weights, returns)
    return -portfolio_return  # Отрицательное, потому что мы минимизируем

def optimize_portfolio(returns, objective='sharpe', target_return=None):
    """
    Оптимизация портфеля на основе различных целей
    """
    num_assets = len(returns.columns)

    # Ограничения и границы
    constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})  # Веса суммируются в 1
    bounds = tuple((0, 1) for _ in range(num_assets))  # Без коротких продаж

    # Начальная догадка
    initial_guess = num_assets * [1. / num_assets]

    if objective == 'sharpe':
        # Максимизируем коэффициент Шарпа
        result = minimize(negative_sharpe_ratio, initial_guess,
                         args=(returns,), method='SLSQP',
                         bounds=bounds, constraints=constraints)

    elif objective == 'min_vol':
        # Минимизируем волатильность
        result = minimize(minimize_volatility, initial_guess,
                         args=(returns,), method='SLSQP',
                         bounds=bounds, constraints=constraints)

    elif objective == 'target_return':
        # Минимизируем волатильность для целевой доходности
        constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1},
                      {'type': 'eq', 'fun': lambda x: portfolio_performance(x, returns)[0] - target_return})

        result = minimize(minimize_volatility, initial_guess,
                         args=(returns,), method='SLSQP',
                         bounds=bounds, constraints=constraints)

    return result

# Находим оптимальные портфели
max_sharpe = optimize_portfolio(returns, 'sharpe')
min_vol = optimize_portfolio(returns, 'min_vol')

print("🎯 Портфель с максимальным коэффициентом Шарпа:")
for i, symbol in enumerate(crypto_symbols):
    print(f"{symbol}: {max_sharpe.x[i]:.3f}")

ret_sharpe, vol_sharpe = portfolio_performance(max_sharpe.x, returns)
print(f"Доходность: {ret_sharpe:.2%}, Волатильность: {vol_sharpe:.2%}")
print(f"Коэффициент Шарпа: {ret_sharpe/vol_sharpe:.3f}\n")

print("🛡️ Портфель с минимальной волатильностью:")
for i, symbol in enumerate(crypto_symbols):
    print(f"{symbol}: {min_vol.x[i]:.3f}")

ret_minvol, vol_minvol = portfolio_performance(min_vol.x, returns)
print(f"Доходность: {ret_minvol:.2%}, Волатильность: {vol_minvol:.2%}")

Уровень 4: Многоактивное безумие - реальный крипто-портфель

Давайте расширим это до настоящего крипто-портфеля с несколькими активами:

# Расширяем до большего количества криптовалют
crypto_portfolio = ['BTC-USD', 'ETH-USD', 'BNB-USD', 'ADA-USD', 'SOL-USD', 'DOT-USD']
prices_multi = get_crypto_data(crypto_portfolio, period="2y")
returns_multi = prices_multi.pct_change().dropna()

# Проверяем корреляции
correlation_matrix = returns_multi.corr()

plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='RdYlBu_r', center=0)
plt.title('Матрица корреляций криптовалютных активов')
plt.show()

def efficient_frontier(returns, num_portfolios=50):
    """
    Расчет границы эффективности
    """
    # Рассчитываем диапазон целевых доходностей
    ret_range = np.linspace(returns.mean().min()*252, returns.mean().max()*252, num_portfolios)

    efficient_portfolios = []

    for target_ret in ret_range:
        try:
            result = optimize_portfolio(returns, 'target_return', target_ret)
            if result.success:
                ret, vol = portfolio_performance(result.x, returns)
                efficient_portfolios.append([ret, vol, result.x])
        except:
            continue

    return np.array(efficient_portfolios)

# Рассчитываем границу эффективности
efficient_port = efficient_frontier(returns_multi)

# Строим всё вместе
plt.figure(figsize=(14, 10))

# Случайные портфели
random_results = generate_random_portfolios(returns_multi, 5000)
plt.scatter(random_results[1], random_results[0],
           c=random_results[2], cmap='viridis', alpha=0.3, s=10)

# Граница эффективности
if len(efficient_port) > 0:
    plt.plot(efficient_port[:,1], efficient_port[:,0], 'r-', linewidth=3, label='Граница эффективности')

# Оптимальные портфели
max_sharpe_multi = optimize_portfolio(returns_multi, 'sharpe')
min_vol_multi = optimize_portfolio(returns_multi, 'min_vol')

ret_sharpe_multi, vol_sharpe_multi = portfolio_performance(max_sharpe_multi.x, returns_multi)
ret_minvol_multi, vol_minvol_multi = portfolio_performance(min_vol_multi.x, returns_multi)

plt.scatter(vol_sharpe_multi, ret_sharpe_multi, marker='*', color='gold', s=500, label='Макс. Шарп')
plt.scatter(vol_minvol_multi, ret_minvol_multi, marker='*', color='red', s=500, label='Мин. волатильность')

plt.colorbar(label='Коэффициент Шарпа')
plt.xlabel('Волатильность (Риск)')
plt.ylabel('Ожидаемая доходность')
plt.title('Оптимизация многоактивного крипто-портфеля')
plt.legend()
plt.show()

# Показываем оптимальные распределения
print("🚀 Оптимальные многоактивные распределения:")
print("\nПортфель с максимальным коэффициентом Шарпа:")
sharpe_weights = pd.Series(max_sharpe_multi.x, index=crypto_portfolio).sort_values(ascending=False)
for asset, weight in sharpe_weights.items():
    if weight > 0.01:  # Показываем только значимые распределения
        print(f"{asset}: {weight:.1%}")

print(f"\nМетрики портфеля:")
print(f"Ожидаемая доходность: {ret_sharpe_multi:.1%}")
print(f"Волатильность: {vol_sharpe_multi:.1%}")
print(f"Коэффициент Шарпа: {ret_sharpe_multi/vol_sharpe_multi:.2f}")

Оптимизация многоактивного портфеля Многоактивная диверсификация: Построение устойчивого портфеля путем объединения некоррелирующих криптоактивов.

Уровень 5: Продвинутые техники - Black-Litterman и Risk Parity

Для настоящих ниндзя оптимизации портфелей, давайте реализуем некоторые продвинутые техники:

def risk_parity_portfolio(returns):
    """
    Портфель Risk Parity - каждый актив вносит равный вклад в риск портфеля
    """
    def risk_contribution(weights, cov_matrix):
        portfolio_vol = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
        marginal_contrib = np.dot(cov_matrix, weights) / portfolio_vol
        contrib = weights * marginal_contrib
        return contrib

    def risk_parity_objective(weights, cov_matrix):
        contrib = risk_contribution(weights, cov_matrix)
        target_contrib = np.ones(len(weights)) / len(weights)
        return np.sum((contrib - target_contrib)**2)

    num_assets = len(returns.columns)
    cov_matrix = returns.cov() * 252

    constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
    bounds = tuple((0.001, 1) for _ in range(num_assets))
    initial_guess = num_assets * [1. / num_assets]

    result = minimize(risk_parity_objective, initial_guess,
                     args=(cov_matrix,), method='SLSQP',
                     bounds=bounds, constraints=constraints)

    return result

# Рассчитываем портфель Risk Parity
risk_parity_result = risk_parity_portfolio(returns_multi)

print("⚖️ Портфель Risk Parity:")
rp_weights = pd.Series(risk_parity_result.x, index=crypto_portfolio).sort_values(ascending=False)
for asset, weight in rp_weights.items():
    print(f"{asset}: {weight:.1%}")

ret_rp, vol_rp = portfolio_performance(risk_parity_result.x, returns_multi)
print(f"\nМетрики Risk Parity:")
print(f"Ожидаемая доходность: {ret_rp:.1%}")
print(f"Волатильность: {vol_rp:.1%}")
print(f"Коэффициент Шарпа: {ret_rp/vol_rp:.2f}")

def backtest_portfolio(weights, prices):
    """
    Простой бэктест производительности портфеля
    """
    returns = prices.pct_change().dropna()
    portfolio_returns = (returns * weights).sum(axis=1)

    # Рассчитываем кумулятивные доходности
    cumulative_returns = (1 + portfolio_returns).cumprod()

    # Метрики производительности
    total_return = cumulative_returns.iloc[-1] - 1
    annualized_return = (1 + total_return) ** (252 / len(portfolio_returns)) - 1
    annualized_vol = portfolio_returns.std() * np.sqrt(252)
    sharpe_ratio = annualized_return / annualized_vol

    max_dd = (cumulative_returns / cumulative_returns.expanding().max() - 1).min()

    return {
        'total_return': total_return,
        'annualized_return': annualized_return,
        'annualized_volatility': annualized_vol,
        'sharpe_ratio': sharpe_ratio,
        'max_drawdown': max_dd,
        'cumulative_returns': cumulative_returns
    }

# Бэктест наших стратегий
strategies = {
    'Макс. Шарп': max_sharpe_multi.x,
    'Мин. волатильность': min_vol_multi.x,
    'Risk Parity': risk_parity_result.x,
    'Равные веса': np.ones(len(crypto_portfolio)) / len(crypto_portfolio)
}

plt.figure(figsize=(14, 8))

for name, weights in strategies.items():
    backtest_results = backtest_portfolio(weights, prices_multi)
    plt.plot(backtest_results['cumulative_returns'], label=f"{name} (Шарп: {backtest_results['sharpe_ratio']:.2f})")

plt.title('Бэктесты стратегий портфеля')
plt.xlabel('Дата')
plt.ylabel('Кумулятивные доходности')
plt.legend()
plt.yscale('log')
plt.grid(True, alpha=0.3)
plt.show()

![Бэктесты стратегий портфеля](/images/blog/markowitz-backtest.webp)
*Алгоритмический бэктестинг: Моделирование исторических результатов для проверки теоретических моделей оптимизации.*


# Сводная таблица производительности
performance_summary = pd.DataFrame()
for name, weights in strategies.items():
    results = backtest_portfolio(weights, prices_multi)
    performance_summary[name] = [
        f"{results['annualized_return']:.1%}",
        f"{results['annualized_volatility']:.1%}",
        f"{results['sharpe_ratio']:.2f}",
        f"{results['max_drawdown']:.1%}"
    ]

performance_summary.index = ['Годовая доходность', 'Годовая волатильность', 'Коэффициент Шарпа', 'Макс. просадка']
print("\n📊 Сводка производительности стратегий:")
print(performance_summary)

Проверка реальностью: о чем не говорит Марковиц

Прежде чем вы полностью погрузитесь в математическую оптимизацию, вот несколько жестких истин о криптовалютах:

1. Прошлые результаты ≠ Будущие результаты Криптовалютный рынок молод и хаотичен. Те корреляции, которые вы рассчитали? Они могут перевернуться за ночь, когда изменятся регулирования или произойдет следующий крупный хак.

2. Транзакционные расходы имеют значение Ребалансировка вашего портфеля стоит денег. В DeFi комиссии за газ могут съесть ваш обед. Учитывайте это в своей стратегии.

3. Проблемы с ликвидностью Не все криптовалюты одинаково ликвидны. Тот токен с малой капитализацией может выглядеть отлично в вашей оптимизации, но попробуйте продать его во время краха.

4. Изменения режимов Криптовалютные рынки имеют разные "режимы" - бычьи рынки, медвежьи рынки, боковые рынки. То, что работает в одном, может не работать в другом.

Практические советы по реализации

def practical_portfolio_rebalancing(target_weights, current_weights, threshold=0.05):
    """
    Ребалансировка только когда веса отклоняются за пределы порога
    """
    weight_diff = np.abs(target_weights - current_weights)
    needs_rebalancing = np.any(weight_diff > threshold)

    if needs_rebalancing:
        print("🔄 Необходима ребалансировка!")
        for i, (target, current) in enumerate(zip(target_weights, current_weights)):
            if abs(target - current) > threshold:
                print(f"Актив {i}: {current:.1%} → {target:.1%}")
    else:
        print("✅ Портфель в пределах допуска, ребалансировка не нужна")

    return needs_rebalancing

# Пример использования
current_allocation = np.array([0.35, 0.25, 0.15, 0.10, 0.10, 0.05])
target_allocation = max_sharpe_multi.x

practical_portfolio_rebalancing(target_allocation, current_allocation)

Заключение: Ваш набор инструментов для оптимизации портфеля

Теперь у вас есть полный набор инструментов для оптимизации крипто-портфеля:

  1. Базовые расчеты для риска и доходности
  2. Визуализация границы эффективности
  3. Математическая оптимизация для различных целей
  4. Продвинутые стратегии как risk parity
  5. Фреймворк бэктестинга для валидации ваших стратегий
  6. Практические соображения для реальной реализации

Ключевые выводы

  • Диверсификация - это бесплатный обед - единственный бесплатный обед в инвестировании
  • Оптимизируйте на основе вашей терпимости к риску - максимальный Шарп не всегда лучший для вас
  • Ребалансируйте систематически, но не торгуйте чрезмерно
  • Оставайтесь скромными - модели это инструменты, а не хрустальные шары
  • Начинайте просто и добавляйте сложность по мере обучения

Помните: В криптовалютах даже лучшие математические модели не могут предсказать, когда Илон твитнет о Dogecoin или когда следующая биржа будет взломана. Используйте портфельную теорию как основу, но всегда держите немного пороха сухим и никогда не инвестируйте больше, чем можете позволить себе потерять.

Теперь идите и оптимизируйте ответственно! 🚀

Дополнительное чтение

Репозиторий кода

Весь код из этого туториала доступен на GitHub: https://github.com/suenot/markowitz

Удачной оптимизации! 📈