暗号資産のためのマーコウィッツポートフォリオ理論:ゼロからヒーローへ

Pythonで最適な暗号資産ポートフォリオを構築する - YOLOは戦略ではないから。ノーベル賞受賞のポートフォリオ理論を暗号資産投資に適用する方法を、実践的なPythonコード例で学びます。

Pythonで最適な暗号資産ポートフォリオを構築する — YOLOは戦略ではないから

マーコウィッツポートフォリオ理論 マーコウィッツポートフォリオ理論:所与のリスク水準でリターンを最大化するために、デジタル資産に適用された数理最適化。


はじめに:暗号資産ポートフォリオに数学が必要な理由(フィーリングだけではダメ)

暗号資産デジェンの皆さん、こんにちは! 👋

イーロンがツイートしたからといってDOGEに全資金を投入した時のことを覚えていますか?それとも前回の暴落で全部パニック売りした時は?ええ、みんな経験があるでしょう。今日はあなたのポートフォリオ(と正気)を救うかもしれないものについて話しましょう:マーコウィッツポートフォリオ理論です。

ハリー・マーコウィッツは1990年にこの研究でノーベル賞を受賞しました。基本的な考え方は?任意のリスクレベルに対して最良のリターンを得られるよう、ポートフォリオを数学的に最適化できるということです。目隠し運転の代わりにGPSを持つようなものです。

核心概念:リスクとリターン(永遠のダンス)

コードに入る前に、何を扱っているか理解しましょう:

  • 期待リターン:どれだけ稼げると期待するか
  • リスク(ボラティリティ):ポートフォリオの価値がどれだけ上下するか
  • 相関:異なる資産がどれだけ似た動きをするか

魔法は完全に同期して動かない資産を組み合わせた時に起こります。ビットコインが暴落しても、一部のDeFiトークンはよく持ちこたえるかもしれません。それが分散投資の力です。

リスクとリターン リスク対リターン:変動性の高い高利回り資産と安定した基盤のバランスを取り、最適な幾何平均リターンを達成する。

Python環境のセットアップ

まず最初に、ツールを準備しましょう:

# Essential libraries for our crypto portfolio optimization
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')

# Set up plotting style
plt.style.use('dark_background')
sns.set_palette("husl")

レベル1:最初の一歩 — シンプルなポートフォリオ数学

基本から始めましょう。シンプルな2資産ポートフォリオのリターンとリスクを計算します。

def get_crypto_data(symbols, period="1y"):
    """
    Fetch crypto data from Yahoo Finance
    symbols: list of crypto symbols (e.g., ['BTC-USD', 'ETH-USD'])
    period: time period for data
    """
    data = yf.download(symbols, period=period)['Adj Close']
    return data

# Get data for Bitcoin and Ethereum
crypto_symbols = ['BTC-USD', 'ETH-USD']
prices = get_crypto_data(crypto_symbols)

# Calculate daily returns
returns = prices.pct_change().dropna()
print("Daily Returns Preview:")
print(returns.head())

基本的なポートフォリオ指標を計算してみましょう:

def portfolio_performance(weights, returns):
    """
    Calculate portfolio return and volatility
    weights: array of portfolio weights
    returns: dataframe of asset returns
    """
    # Portfolio return (annualized)
    portfolio_return = np.sum(returns.mean() * weights) * 252

    # Portfolio volatility (annualized)
    portfolio_vol = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))

    return portfolio_return, portfolio_vol

# Example: 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 Portfolio:")
print(f"Expected Annual Return: {ret_5050:.2%}")
print(f"Annual Volatility: {vol_5050:.2%}")
print(f"Sharpe Ratio: {ret_5050/vol_5050:.3f}")

レベル2:本格的に — 効率的フロンティア

いよいよ本番です!効率的フロンティアは、すべての最適なポートフォリオを示します。各点は、所与のリスクレベルに対する最良のリターンを表します。

def generate_random_portfolios(returns, num_portfolios=10000):
    """
    Generate random portfolio combinations
    """
    num_assets = len(returns.columns)
    results = np.zeros((4, num_portfolios))

    for i in range(num_portfolios):
        # Generate random weights
        weights = np.random.random(num_assets)
        weights /= np.sum(weights)  # Normalize to sum to 1

        # Calculate portfolio metrics
        portfolio_return, portfolio_vol = portfolio_performance(weights, returns)
        sharpe_ratio = portfolio_return / portfolio_vol

        # Store results
        results[0,i] = portfolio_return
        results[1,i] = portfolio_vol
        results[2,i] = sharpe_ratio
        results[3,i:] = weights

    return results

# Generate 10,000 random portfolios
results = generate_random_portfolios(returns)

# Create a dataframe for easier handling
portfolio_results = pd.DataFrame({
    'Returns': results[0],
    'Volatility': results[1],
    'Sharpe_Ratio': results[2]
})

# Plot the efficient frontier
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='Sharpe Ratio')
plt.xlabel('Volatility (Risk)')
plt.ylabel('Expected Return')
plt.title('Efficient Frontier - Random Portfolios')
plt.show()

効率的フロンティア 効率的フロンティア:所与のリスクレベルに対する最大期待リターンを表す曲線。

レベル3:最適化マスター — 完璧なポートフォリオを見つける

ランダムサンプリングは楽しいですが、数学的に最適な解を求めましょう。scipyの最適化の出番です!

def negative_sharpe_ratio(weights, returns, risk_free_rate=0.02):
    """
    Calculate negative Sharpe ratio (we minimize this)
    """
    portfolio_return, portfolio_vol = portfolio_performance(weights, returns)
    sharpe = (portfolio_return - risk_free_rate) / portfolio_vol
    return -sharpe

def minimize_volatility(weights, returns):
    """
    Calculate portfolio volatility (we minimize this)
    """
    _, portfolio_vol = portfolio_performance(weights, returns)
    return portfolio_vol

def portfolio_return_objective(weights, returns):
    """
    Calculate portfolio return (we maximize this)
    """
    portfolio_return, _ = portfolio_performance(weights, returns)
    return -portfolio_return  # Negative because we minimize

def optimize_portfolio(returns, objective='sharpe', target_return=None):
    """
    Optimize portfolio based on different objectives
    """
    num_assets = len(returns.columns)

    # Constraints and bounds
    constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})  # Weights sum to 1
    bounds = tuple((0, 1) for _ in range(num_assets))  # No short selling

    # Initial guess
    initial_guess = num_assets * [1. / num_assets]

    if objective == 'sharpe':
        # Maximize Sharpe ratio
        result = minimize(negative_sharpe_ratio, initial_guess,
                         args=(returns,), method='SLSQP',
                         bounds=bounds, constraints=constraints)

    elif objective == 'min_vol':
        # Minimize volatility
        result = minimize(minimize_volatility, initial_guess,
                         args=(returns,), method='SLSQP',
                         bounds=bounds, constraints=constraints)

    elif objective == 'target_return':
        # Minimize volatility for 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

# Find optimal portfolios
max_sharpe = optimize_portfolio(returns, 'sharpe')
min_vol = optimize_portfolio(returns, 'min_vol')

print("🎯 Maximum Sharpe Ratio Portfolio:")
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"Return: {ret_sharpe:.2%}, Volatility: {vol_sharpe:.2%}")
print(f"Sharpe Ratio: {ret_sharpe/vol_sharpe:.3f}\n")

print("🛡️ Minimum Volatility Portfolio:")
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"Return: {ret_minvol:.2%}, Volatility: {vol_minvol:.2%}")

レベル4:マルチアセットの世界 — 本格的な暗号資産ポートフォリオ

複数の資産を含む本格的な暗号資産ポートフォリオにスケールアップしましょう:

# Expand to more cryptocurrencies
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()

# Check correlations
correlation_matrix = returns_multi.corr()

plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='RdYlBu_r', center=0)
plt.title('Crypto Asset Correlation Matrix')
plt.show()

def efficient_frontier(returns, num_portfolios=50):
    """
    Calculate the efficient frontier
    """
    # Calculate range of target returns
    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)

# Calculate efficient frontier
efficient_port = efficient_frontier(returns_multi)

# Plot everything together
plt.figure(figsize=(14, 10))

# Random portfolios
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)

# Efficient frontier
if len(efficient_port) > 0:
    plt.plot(efficient_port[:,1], efficient_port[:,0], 'r-', linewidth=3, label='Efficient Frontier')

# Optimal portfolios
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='Max Sharpe')
plt.scatter(vol_minvol_multi, ret_minvol_multi, marker='*', color='red', s=500, label='Min Volatility')

plt.colorbar(label='Sharpe Ratio')
plt.xlabel('Volatility (Risk)')
plt.ylabel('Expected Return')
plt.title('Multi-Asset Crypto Portfolio Optimization')
plt.legend()
plt.show()

# Display optimal allocations
print("🚀 Optimal Multi-Asset Allocations:")
print("\nMaximum Sharpe Ratio Portfolio:")
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:  # Only show significant allocations
        print(f"{asset}: {weight:.1%}")

print(f"\nPortfolio Metrics:")
print(f"Expected Return: {ret_sharpe_multi:.1%}")
print(f"Volatility: {vol_sharpe_multi:.1%}")
print(f"Sharpe Ratio: {ret_sharpe_multi/vol_sharpe_multi:.2f}")

マルチアセットポートフォリオ最適化 マルチアセット分散投資:相関の低い暗号資産を組み合わせて、安定した幾何学的構造の堅牢なポートフォリオを構築する。

レベル5:上級テクニック — ブラック・リッターマンとリスクパリティ

真のポートフォリオ最適化の達人のために、上級テクニックを実装しましょう:

def risk_parity_portfolio(returns):
    """
    Risk Parity Portfolio - each asset contributes equally to portfolio risk
    """
    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

# Calculate Risk Parity portfolio
risk_parity_result = risk_parity_portfolio(returns_multi)

print("⚖️ Risk Parity Portfolio:")
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"\nRisk Parity Metrics:")
print(f"Expected Return: {ret_rp:.1%}")
print(f"Volatility: {vol_rp:.1%}")
print(f"Sharpe Ratio: {ret_rp/vol_rp:.2f}")

def backtest_portfolio(weights, prices):
    """
    Simple backtest of portfolio performance
    """
    returns = prices.pct_change().dropna()
    portfolio_returns = (returns * weights).sum(axis=1)

    # Calculate cumulative returns
    cumulative_returns = (1 + portfolio_returns).cumprod()

    # Performance metrics
    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
    }

# Backtest our strategies
strategies = {
    'Max Sharpe': max_sharpe_multi.x,
    'Min Volatility': min_vol_multi.x,
    'Risk Parity': risk_parity_result.x,
    'Equal Weight': 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} (Sharpe: {backtest_results['sharpe_ratio']:.2f})")

plt.title('Portfolio Strategy Backtests')
plt.xlabel('Date')
plt.ylabel('Cumulative Returns')
plt.legend()
plt.yscale('log')
plt.grid(True, alpha=0.3)
plt.show()

![Portfolio Strategy Backtests](/images/blog/markowitz-backtest.webp)
*アルゴリズミック・バックテスト:理論的最適化モデルを検証するためにヒストリカルパフォーマンスをシミュレーション。*


# Performance summary table
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 = ['Annual Return', 'Annual Volatility', 'Sharpe Ratio', 'Max Drawdown']
print("\n📊 Strategy Performance Summary:")
print(performance_summary)

現実チェック:マーコウィッツが教えてくれないこと

数学的最適化に全力投入する前に、暗号資産についてのいくつかの厳しい真実があります:

1. 過去のパフォーマンス ≠ 将来の結果 暗号資産市場は若く混沌としています。計算した相関は?規制が変わったり、次の大きなハッキングが起きたりすると、一夜にして反転する可能性があります。

2. 取引コストが重要 ポートフォリオのリバランスにはコストがかかります。DeFiではガス代が利益を食い潰すことがあります。戦略にこれを組み込みましょう。

3. 流動性の問題 すべての暗号資産が均等に流動的なわけではありません。最適化では小型のアルトコインが素晴らしく見えるかもしれませんが、暴落時に売ってみてください。

4. レジーム変化 暗号資産市場には異なる「レジーム」があります — 強気相場、弱気相場、横ばい相場。一つのレジームでうまくいくものが、別のレジームではうまくいかない可能性があります。

実践的な実装のヒント

def practical_portfolio_rebalancing(target_weights, current_weights, threshold=0.05):
    """
    Only rebalance when weights drift beyond threshold
    """
    weight_diff = np.abs(target_weights - current_weights)
    needs_rebalancing = np.any(weight_diff > threshold)

    if needs_rebalancing:
        print("🔄 Rebalancing needed!")
        for i, (target, current) in enumerate(zip(target_weights, current_weights)):
            if abs(target - current) > threshold:
                print(f"Asset {i}: {current:.1%} → {target:.1%}")
    else:
        print("✅ Portfolio within tolerance, no rebalancing needed")

    return needs_rebalancing

# Example usage
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. リスクパリティなどの上級戦略
  5. 戦略を検証するバックテストフレームワーク
  6. 実世界での実装のための実践的考慮事項

重要なポイント

  • 分散投資はフリーランチ — 投資における唯一のフリーランチ
  • リスク許容度に基づいて最適化 — 最大シャープが常にベストとは限らない
  • 体系的にリバランスするが、過度な取引は避ける
  • 謙虚でいる — モデルはツールであり、水晶玉ではない
  • シンプルに始めて、学びながら複雑さを加える

覚えておいてください:暗号資産では、最高の数学モデルでさえ、イーロンがいつDogecoinについてツイートするか、次にどの取引所がハッキングされるかは予測できません。ポートフォリオ理論を基盤として使いつつ、常に余力を残し、失っても大丈夫な金額以上は投資しないでください。

さあ、責任を持って最適化しましょう! 🚀

参考文献

コードリポジトリ

このチュートリアルのすべてのコードはGitHubで利用可能です: https://github.com/suenot/markowitz

Happy optimizing! 📈