Pythonでポートフォリオ最適化:マーコウィッツ理論で効率的フロンティアと最適ウェイトを計算

Python実装・コード

マーコウィッツのポートフォリオ理論は、複数の資産を組み合わせてリターンを最大化しながらリスクを最小化するフレームワークです。ということで、この記事ではPythonで効率的フロンティアを計算し、最適ポートフォリオのウェイトを求める実装をまとめます。

📘 外部参考Python 公式(ダウンロード)Python 公式ドキュメント(日本語)

📘 外部参考Efficient Frontier(Investopedia)

📘 外部参考現代ポートフォリオ理論(Wikipedia 日本語)Modern Portfolio Theory(Investopedia)

ライブラリのインストールとデータ取得

pip install yfinance pandas numpy scipy matplotlib
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from scipy.optimize import minimize

# 日本株ポートフォリオ候補
tickers = ['7203.T', '6758.T', '9984.T', '4063.T', '6861.T']
names = ['トヨタ', 'ソニー', 'ソフトバンクG', '信越化学', 'キーエンス']

def get_returns(tickers, period='3y'):
    data = yf.download(tickers, period=period, progress=False)['Close']
    returns = data.pct_change().dropna()
    return returns

returns = get_returns(tickers)
print("年率リターン:")
print((returns.mean() * 252).to_string())
print("\n年率ボラティリティ:")
print((returns.std() * np.sqrt(252)).to_string())

ポートフォリオのリターン・リスク計算

def portfolio_stats(weights, returns):
    weights = np.array(weights)
    ann_return = np.sum(returns.mean() * weights) * 252
    ann_vol = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))
    sharpe = ann_return / ann_vol
    return ann_return, ann_vol, sharpe

# 等加重ポートフォリオの確認
n = len(tickers)
equal_weights = np.array([1/n] * n)
ret, vol, sharpe = portfolio_stats(equal_weights, returns)
print(f"等加重ポートフォリオ")
print(f"  期待リターン: {ret:.2%}")
print(f"  リスク(標準偏差): {vol:.2%}")
print(f"  シャープレシオ: {sharpe:.2f}")

効率的フロンティアの計算

def efficient_frontier(returns, n_points=200):
    n = len(returns.columns)
    results = []
    
    for _ in range(n_points):
        # ランダムウェイトを生成して効率的フロンティアをモンテカルロでプロット
        w = np.random.dirichlet(np.ones(n))
        ret, vol, sharpe = portfolio_stats(w, returns)
        results.append({'return': ret, 'vol': vol, 'sharpe': sharpe, 'weights': w})
    
    return pd.DataFrame(results)

def find_optimal_portfolio(returns, objective='sharpe'):
    n = len(returns.columns)
    constraints = [{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}]
    bounds = [(0.05, 0.4)] * n  # 各銘柄5%〜40%に制限
    initial = np.array([1/n] * n)
    
    if objective == 'sharpe':
        def neg_sharpe(w):
            return -portfolio_stats(w, returns)[2]
        result = minimize(neg_sharpe, initial, bounds=bounds, constraints=constraints)
    elif objective == 'min_vol':
        def portfolio_vol(w):
            return portfolio_stats(w, returns)[1]
        result = minimize(portfolio_vol, initial, bounds=bounds, constraints=constraints)
    
    opt_weights = result.x
    opt_ret, opt_vol, opt_sharpe = portfolio_stats(opt_weights, returns)
    return opt_weights, opt_ret, opt_vol, opt_sharpe

# 最大シャープレシオポートフォリオ
opt_w, opt_ret, opt_vol, opt_sharpe = find_optimal_portfolio(returns, 'sharpe')
print("最大シャープレシオ ポートフォリオ")
for ticker, name, w in zip(tickers, names, opt_w):
    print(f"  {name}({ticker}): {w:.1%}")
print(f"  期待リターン: {opt_ret:.2%}")
print(f"  リスク: {opt_vol:.2%}")
print(f"  シャープレシオ: {opt_sharpe:.2f}")

リバランシング戦略の実装

def rebalancing_backtest(returns, rebalance_freq='Q', objective='sharpe'):
    """
    定期リバランシング戦略のバックテスト
    rebalance_freq: 'M'=毎月, 'Q'=四半期, 'Y'=年次
    """
    portfolio_value = 1.0
    current_weights = np.array([1/len(returns.columns)] * len(returns.columns))
    history = []
    
    rebalance_dates = pd.date_range(returns.index[60], returns.index[-1], freq=rebalance_freq)
    
    for date in rebalance_dates:
        # この時点までのデータで最適化
        hist_data = returns[returns.index <= date].tail(252)
        if len(hist_data) < 60:
            continue
        
        try:
            opt_w, _, _, _ = find_optimal_portfolio(hist_data, objective)
            current_weights = opt_w
        except Exception:
            pass
        
        # 次のリバランスまでのリターンを計算
        next_dates = returns[(returns.index > date)].index
        if len(next_dates) == 0:
            break
        next_rebalance = rebalance_dates[rebalance_dates > date][0] if any(rebalance_dates > date) else returns.index[-1]
        period_returns = returns[(returns.index > date) & (returns.index <= next_rebalance)]
        
        if len(period_returns) > 0:
            period_portfolio_return = (period_returns * current_weights).sum(axis=1).add(1).prod() - 1
            portfolio_value *= (1 + period_portfolio_return)
            history.append({'date': next_rebalance, 'value': portfolio_value})
    
    total_return = portfolio_value - 1
    print(f"リバランシング({rebalance_freq})戦略 総リターン: {total_return:.2%}")
    return pd.DataFrame(history)

result_q = rebalancing_backtest(returns, 'Q')
result_m = rebalancing_backtest(returns, 'M')

まとめ

Pythonによるマーコウィッツのポートフォリオ最適化のポイントをまとめます。分散投資の効果は銘柄間の相関係数に依存し、低相関な銘柄の組み合わせがリスク低減に有効です。シャープレシオ最大化の最適化は、scipyのminimizeで制約付き非線形最適化として解けます。銘柄数が増えると共分散行列の推定誤差が大きくなるため、最小分散ポートフォリオや制約付き最適化が実用的です。定期的なリバランシング(四半期ごと等)で、市場変動によるウェイトのズレを修正することが長期運用では重要です。過去データで最適化されたウェイトが将来も最適とは限らないため、ロバスト最適化や正則化手法の併用を検討することをお勧めします。

📘 外部参考シャープ・レシオ(Wikipedia)Investopedia

🔗 関連記事

Pythonで複数銘柄の株価を比較する方法【正規化・相関分析】

タイトルとURLをコピーしました