ChatGPTで株のポートフォリオ最適化スクリプトを作った

# 目的
ChatGPTで株のポートフォリオ最適化スクリプトを作ってその実力を確かめました。

今回はモンテカルロシミュレーション手法とブラック・リッターマン手法の２つを試してみました。結果からするとモンテカルロシミュレーションの方が、時間はかかるがもっともらしい結果を出しました。ブラック・リッターマンはかなり偏った結果になってしまった。多分選ぶ銘柄とか期待値の設定とかで大きく変わるのだと思う。

# 解析手法（解説 by ChatGPT4o）
::: note info
## 簡単に言うと
- **モンテカルロ・シミュレーション**は、大量のシナリオを生成してリスクとリターンを評価する手法であり、主に確率論に基づいています。
- **ブラック・リッターマンの手法**は、投資家の見通しと市場データを組み合わせて資産の期待リターンを推定し、それに基づいてポートフォリオを最適化する手法です。

## モンテカルロ・シミュレーション
モンテカルロ・シミュレーションは、確率論と統計を用いてポートフォリオの将来のパフォーマンスを予測する手法です。この手法は、大量のシナリオを生成し、それらのシナリオに基づいてリスクとリターンを評価します。

1. **ステップ1: 初期条件の設定**
   - 現在のポートフォリオの構成
   - 各資産のリターンの分布（平均リターンと標準偏差）
   - 相関係数

2. **ステップ2: シナリオの生成**
   - 各資産のリターンをランダムに生成する。これには、通常分布などの統計分布を使用する。

3. **ステップ3: ポートフォリオのシミュレーション**
   - 各シナリオにおいてポートフォリオの価値を計算する。
   - 例えば、1年間のリターンを計算する場合、各月のリターンを積み上げて年間リターンを求める。

4. **ステップ4: 結果の分析**
   - シナリオの結果を集計し、リスク（標準偏差やVaRなど）とリターンを評価する。
   - 最悪のケース、中央値、最良のケースなどを分析する。

## ブラック・リッターマンの手法
ブラック・リッターマンの手法は、投資家の見通しをポートフォリオ最適化に取り入れることを目的とした手法です。これは、資産価格の期待リターンを推定するための手法であり、投資家の主観的な見解と市場のデータを組み合わせて使用します。

1. **市場均衡リターンの計算**
   - 市場のキャピタル・アセット・プライシング・モデル（CAPM）を用いて、各資産の均衡リターンを計算する。

2. **投資家の見通しの取り入れ**
   - 投資家は、特定の資産や資産グループに対して自分の見通し（例えば、ある資産が市場全体よりも良いパフォーマンスをする）を入力する。

3. **投資家の見通しと市場均衡リターンの結合**
   - ベイズの定理を用いて、投資家の見通しと市場均衡リターンを結合し、新しいリターン予測を作成する。
   - この過程では、投資家の見通しの確からしさ（信頼度）も考慮される。

4. **ポートフォリオの最適化**
   - 新しいリターン予測に基づいて、ポートフォリオの最適化を行う。
   - マーコビッツの平均分散最適化の手法が一般的に用いられる。
:::

# モンテカルロ・シミュレーション スクリプト（Generated by ChatGPT4o）
``` Python
import numpy as np
import pandas as pd
import yfinance as yf
from scipy.stats import norm
import matplotlib.pyplot as plt
from tqdm import tqdm
from datetime import datetime

def get_data(tickers, start_date, end_date):
    data = yf.download(tickers, start=start_date, end=end_date)['Adj Close']
    returns = data.pct_change().dropna()
    return returns

def cholesky_decomposition(cov_matrix):
    return np.linalg.cholesky(cov_matrix)

def simulate_returns(mean_returns, cov_matrix, num_simulations, time_horizon):
    num_assets = len(mean_returns)
    L = cholesky_decomposition(cov_matrix)

    simulated_returns = np.zeros((num_simulations, num_assets, time_horizon))

    for i in range(num_simulations):
        Z = np.random.normal(0, 1, (num_assets, time_horizon))
        daily_returns = mean_returns.values.reshape(-1, 1) + np.dot(L, Z)
        simulated_returns[i] = daily_returns

    return simulated_returns

def calculate_portfolio_values(initial_weights, simulated_returns, initial_value=10000):
    num_simulations, num_assets, time_horizon = simulated_returns.shape
    portfolio_values = np.zeros((num_simulations, time_horizon))

    for i in range(num_simulations):
        portfolio_values[i, 0] = initial_value
        for t in range(1, time_horizon):
            portfolio_return = np.sum(initial_weights * simulated_returns[i, :, t])
            portfolio_values[i, t] = portfolio_values[i, t-1] * (1 + portfolio_return)

    return portfolio_values

def calculate_risk_metrics(portfolio_values):
    final_values = portfolio_values[:, -1]
    initial_value = portfolio_values[0, 0]

    total_return = (final_values - initial_value) / initial_value
    mean_return = np.mean(total_return)
    volatility = np.std(total_return)

    var_95 = np.percentile(final_values, 5)
    cvar_95 = np.mean(final_values[final_values <= var_95])

    sharpe_ratio = mean_return / volatility

    return {
        'mean_return': mean_return,
        'volatility': volatility,
        'var_95': (initial_value - var_95) / initial_value,
        'cvar_95': (initial_value - cvar_95) / initial_value,
        'sharpe_ratio': sharpe_ratio
    }

def optimize_portfolio(mean_returns, cov_matrix, num_simulations, time_horizon, num_portfolios):
    num_assets = len(mean_returns)
    results = []

    for _ in tqdm(range(num_portfolios)):
        weights = np.random.dirichlet(np.ones(num_assets), size=1)[0]
        simulated_returns = simulate_returns(mean_returns, cov_matrix, num_simulations, time_horizon)
        portfolio_values = calculate_portfolio_values(weights, simulated_returns)
        risk_metrics = calculate_risk_metrics(portfolio_values)

        results.append({
            'weights': weights,
            **risk_metrics
        })

    return pd.DataFrame(results)

# メイン処理
tickers = ['FXI', 'SMH', 'VWO', 'VOO', 'SPY', 'ACWI', 'BND', 'TIP', 'TLT', 'GLD', 'IAU',]
start_date = '2014-01-01'
end_date = datetime.today().strftime('%Y-%m-%d')

returns = get_data(tickers, start_date, end_date)
mean_returns = returns.mean()
cov_matrix = returns.cov()

num_simulations = 10000
time_horizon = 252  # 1年の取引日数
num_portfolios = 1000

results = optimize_portfolio(mean_returns, cov_matrix, num_simulations, time_horizon, num_portfolios)

# 結果の表示とプロット
plt.figure(figsize=(10, 6))
plt.scatter(results['volatility'], results['mean_return'], c=results['sharpe_ratio'], cmap='viridis')
plt.colorbar(label='Sharpe Ratio')
plt.xlabel('Volatility')
plt.ylabel('Mean Return')
plt.title('Efficient Frontier with Monte Carlo Simulation')

# 最適ポートフォリオの表示
optimal_portfolio = results.loc[results['sharpe_ratio'].idxmax()]
print("Optimal Portfolio:")
for ticker, weight in zip(tickers, optimal_portfolio['weights']):
    print(f"{ticker}: {weight:.2%}")
print(f"Expected Return: {optimal_portfolio['mean_return']:.2%}")
print(f"Volatility: {optimal_portfolio['volatility']:.2%}")
print(f"Sharpe Ratio: {optimal_portfolio['sharpe_ratio']:.2f}")
print(f"VaR (95%): {optimal_portfolio['var_95']:.2%}")
print(f"CVaR (95%): {optimal_portfolio['cvar_95']:.2%}")

plt.show()
```

## モンテカルロ・シミュレーション 出力
```
Optimal Portfolio:
FXI: 1.04%
SMH: 21.07%
VWO: 1.67%
VOO: 2.18%
SPY: 24.50%
ACWI: 23.00%
BND: 17.14%
TIP: 5.20%
TLT: 1.33%
GLD: 2.27%
IAU: 0.59%
Expected Return: 12.94%
Volatility: 12.68%
Sharpe Ratio: 1.02
VaR (95%): 6.91%
CVaR (95%): 11.19%
```

## モンテカルロ・シミュレーション 結果の考察（by ChatGPT4o）

ポートフォリオの比率を見ると、まあ割と理にかなった割合になっています。リスクが高そうなSMHの割合がちょっと高めなのが気になりますが、債権とコモディティの割合もまあいいんじゃないかという感じ。

以下、ChatGPT4oのこのポートフォリオの解説

::: note info
モンテカルロ・シミュレーションの結果を考察する際には、ポートフォリオの構成、期待リターン、ボラティリティ（リスク）、シャープレシオ、VaR（バリュー・アット・リスク）、およびCVaR（条件付きバリュー・アット・リスク）を総合的に評価する必要があります。

### ポートフォリオの構成
ポートフォリオは以下のような構成になっています：
- FXI: 1.04%
- SMH: 21.07%
- VWO: 1.67%
- VOO: 2.18%
- SPY: 24.50%
- ACWI: 23.00%
- BND: 17.14%
- TIP: 5.20%
- TLT: 1.33%
- GLD: 2.27%
- IAU: 0.59%

この構成を見ると、ポートフォリオは比較的分散されており、特に株式（SMH、SPY、ACWI）と債券（BND、TIP）が主要な構成要素となっています。また、少量の金（GLD、IAU）が含まれており、これによってリスク分散が図られています。

### 期待リターンとボラティリティ
- **期待リターン: 12.94%**
- **ボラティリティ: 12.68%**

期待リターンが12.94%で、ボラティリティが12.68%という結果は、比較的高いリターンを見込んでいる一方で、リスクもそれなりに高いことを示しています。このリターンとリスクのバランスが適切かどうかは、投資家のリスク許容度や目標によります。

### シャープレシオ
- **シャープレシオ: 1.02**

シャープレシオはリスク調整後のリターンを示す指標で、1.02という値はポートフォリオがリスクに見合ったリターンを提供していることを示しています。シャープレシオが1以上であれば、通常は良好なパフォーマンスとされます。

### VaRとCVaR
- **VaR (95%): 6.91%**
- **CVaR (95%): 11.19%**

VaRは、ポートフォリオが95%の確率で、ある期間内にこれ以上の損失を被らないとする最大損失額を示します。CVaRは、VaRを超えた場合の平均損失額を示します。VaRが6.91%であることは、95%の確率でポートフォリオの損失が6.91%以内に収まることを意味します。CVaRが11.19%であることは、最悪の5%のケースで平均損失が11.19%になることを示します。

### 総合評価
1. **リターンとリスクのバランス**：期待リターンが高く、シャープレシオも1.02と良好です。しかし、リスクもそれなりに高いことに注意が必要です。
2. **リスク管理**：VaRとCVaRの値から、最悪のケースシナリオを考慮する必要があります。特に、CVaRが示すように、最悪のシナリオでの損失が大きいことに対する対策を考えるべきです。
3. **分散投資**：株式、債券、金などの多様な資産クラスに分散されており、リスク分散が図られています。

このポートフォリオは、リターンを追求する一方で、リスク管理も適切に行われているバランスの取れた構成と言えます。ただし、個々の投資家のリスク許容度や投資目標に応じて、さらなる調整が必要かもしれません。
:::

## ブラック・リッターマン スクリプト（Generated by ChatGPT4o）
``` Python
import yfinance as yf
import pandas as pd
import numpy as np
from pypfopt import risk_models, expected_returns, BlackLittermanModel, EfficientFrontier

# ティッカー
tickers = ['FXI', 'SMH', 'VWO', 'VOO', 'SPY', 'ACWI', 'BND', 'TIP', 'TLT', 'GLD', 'IAU']

# データ取得期間
start_date = '2014-01-01'
end_date = '2024-07-14'

# Yahoo Financeから価格データを取得
df = yf.download(tickers, start=start_date, end=end_date)['Adj Close']

# 期待リターンと共分散行列を計算
mu = expected_returns.mean_historical_return(df)
S = risk_models.sample_cov(df)

# 市場ポートフォリオを定義 (時価総額加重)
market_caps = {ticker: yf.Ticker(ticker).info.get('totalAssets') for ticker in tickers}
market_caps = {ticker: cap for ticker, cap in market_caps.items() if cap is not None}
total_market_cap = sum(market_caps.values())
market_prior = {ticker: cap / total_market_cap for ticker, cap in market_caps.items()}
market_prior_series = pd.Series(market_prior)

# Views (期待超過リターン、過去リターンデータに基づく)
views = mu.to_dict()

# Views confidences (信頼度、0-1の範囲)
view_confidences = [
    0.6,
    0.7,
    0.5,
    0.8,
    0.8,
    0.7,
    0.6,
    0.5,
    0.6,
    0.4,
    0.4
]

# ブラック・リッターマン・モデルを構築
bl = BlackLittermanModel(S, pi=market_prior_series, absolute_views=views, omega="idzorek", view_confidences=view_confidences)
rets = bl.bl_returns()

# 最適化
ef = EfficientFrontier(rets, S)
weights = ef.max_sharpe()
cleaned_weights = ef.clean_weights()

# 結果表示
print("最適化されたポートフォリオの重み:")
for ticker, weight in cleaned_weights.items():
    print(f"{ticker}: {weight:.2%}")

# その他の分析 (例: 年間リターンと年間ボラティリティ)
ef.portfolio_performance(verbose=True)
```

## ブラック・リッターマン 出力
金とSPYにすごく偏ってしまった。まあ合理的といえば合理的なんですけど。viewsとかview_confidencesをいろいろ調整しないといけないのかも。あとωの計算手法でもいろいろ変わるらしい。

```
最適化されたポートフォリオの重み:
ACWI: 0.00%
BND: 0.00%
FXI: 0.00%
GLD: 63.81%
IAU: 0.00%
SMH: 0.00%
SPY: 36.19%
TIP: 0.00%
TLT: 0.00%
VOO: 0.00%
VWO: 0.00%
Expected annual return: 22.5%
Annual volatility: 11.1%
Sharpe Ratio: 1.85
```

# まとめ
モンテカルロ・シミュレーションはポートフォリををいろんな組み合わせで過去のデータで評価する、というやり方なので理解しやすいし、出力もまあ無難な気がする。ブラック・リッターマンはちょっとよくわからなかった。もっとたくさんの個別銘柄を選んでやったら結果は変わるのかもしれない。