К списку статей
September 25, 2025
5 мин. чтения

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

оптимизация портфеля
Марковиц
криптовалюты
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

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

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

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_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()

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 или когда следующая биржа будет взломана. Используйте портфельную теорию как основу, но всегда держите немного пороха сухим и никогда не инвестируйте больше, чем можете позволить себе потерять.

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


Дисклеймер: Это только для образовательных целей. Не является финансовым советом. DYOR. Рынок криптовалют высоко волатилен и рискован. Прошлые результаты не гарантируют будущих результатов.

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

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

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

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

MarketMaker.cc Team

Количественные исследования и стратегии

Обсудить в Telegram