現代ポートフォリオ理論と最大SRポートフォリオの分析

### はじめに
本日は、Stefan Jasen氏によって書かれた["Machine Learning for Algorithmic trading, 2nd edition"のChapter 5, Mean-variance optimization](https://github.com/Tokiwabashi27/Machine-Learning-for-Algorithmic-Trading-Second-Edition/tree/master/05_strategy_evaluation)について勉強した内容をまとめます。Pythonを用いたコーディングに際して、[カナヲ定量分析さんの記事](https://note.com/kanawoinvestment/n/n1fa57800c26f)を参考にさせて頂きました。Stefan氏の解説だけでは理解できない部分を補うことができました。ありがとうございます。

### 現代ポートフォリオ理論の考え方

- 同じ期待リターンならvolが小さいPFを選択することが、もっとも好ましい。
- 各期待リターンにおいてvolが最も小さいPFの集合を効率的フロンティアと呼ぶ。

### コードの概要

- yahoofinanceからSP500にリストされている30の株（クリーニングの過程で27に絞られる）を、Fredからrisk-free rateを取得。
- ディリクレ分布より重みを1万通りサンプリングし、それらを用いて運用した結果をプロット。また、最大SRPF・最小分散PFをプロット。
- scipy.optimizeからminimizeを用いてSRが最大化する重みを推定。また、その重みを用いた運用結果を計算。


```Python
#Get data from yfinance using pandas_DataReader
import numpy as np
import pandas as pd
import pandas_datareader as pdr
import datetime
import matplotlib.pyplot as plt
import seaborn as sns
from numpy.linalg import inv
from numpy.random import random, uniform, dirichlet, choice
from scipy.optimize import minimize

tickers = ['MMM','ABT','ACN','AFL','ARE','GOOGL','AMZN','AME','APA','AAPL'
                      ,'BKR','CPB','KMX','CCL','DHR','CVS','ECL','EA','EIX','FOXA','BEN'
                      ,'FCX','HCA','IP','KEY','LNC','MTCH','MS','NLSN','PVH']

df = pdr.DataReader(tickers, data_source ='yahoo', start ='2010-01-01', end = '2021-01-01');

#delete any rows with NaN
df = df.dropna(how = 'any',axis = 1);

#clean the df
df = df['Adj Close'];

#compute weekly returns
weekly_returns = df.resample('W').last().pct_change().dropna(axis=0, how ='all');

#interpret stocks as the indices(['MMM',...,'PVH'])
stocks = weekly_returns.columns

#the number of data points and stocks
n_obs, n_assets = weekly_returns.shape

#the number of data points and stocks
n_obs, n_assets = weekly_returns.shape

#the number of simulation
NUM_PF = 10000

#set the range of portfolio weights
from numpy.random import random, uniform, dirichlet, choice 
x0 = uniform(0,1,n_assets)

#x0 = xo / np.sum(np.abs(x0))
x0 /=np.sum(np.abs(x0))

#一年に何回リターンが計測されているか平均を出す
periods_per_year = round(weekly_returns.resample('A').size().mean())
periods_per_year

#compute average return, covariance, inverse covariance matrix

mean_returns = weekly_returns.mean()
cov_matrix = weekly_returns.cov()
inverse_cov_matrix = pd.DataFrame(inv(cov_matrix), index = stocks, columns = stocks)

#compute risk-free rate using Fred
df2 = pdr.DataReader('^TNX', data_source ='yahoo', start ='2010-01-01', end = '2021-01-01');
df2 = df2['Adj Close']

#リターン一つ分に対応するリスクフリーリターン
treasury_10yr_monthly = (df2.resample('M').last().div(periods_per_year).div(100).squeeze())
rf_rate = treasury_10yr_monthly.mean()

#simulate random portfolio
def simulate_portfolios(mean_ret, cov, rf_rate = rf_rate, short=True):
    alpha = np.full(shape=n_assets, fill_value=.05) #0.05がn-assets分含まれたndarrayを作成
    weights = dirichlet(alpha=alpha, size=NUM_PF) #各アセットの係数の組み合わせをNUM_PF分作成
    if short:
        weights *= choice([-1,1],size=weights.shape)#-1
        
    returns = weights @ mean_ret.values +1 #重みと平均リターン行列の積
    returns = returns ** periods_per_year -1#年率リターン
    std = (weights @ weekly_returns.T).std(1)#週ごとのvolを計算
    std *=np.sqrt(periods_per_year)#週率リスクから年率リスクへ変換
    sharpe = (returns - rf_rate)/std
    return pd.DataFrame({'Annualized Standard Deviation':std, 
                        'Annualized Returns':returns, 
                        'Sharpe Ratio': sharpe}), weights;

#simul_perfにはdfを、simul_wtにはalpha=0.05のディリクレ分布から取得した係数を格納
simul_perf, simul_wt = simulate_portfolios(mean_returns, cov_matrix, short=False);
df = pd.DataFrame(simul_wt);

#axesおよびx-axis,y-axisを設定する
axes = simul_perf.plot(kind='scatter', x="Annualized Standard Deviation",
                               y="Annualized Returns",c = 2, cmap ='Blues',alpha=0.5, figsize=(14,9), colorbar=True,
                              title='{} Simulated Portfolios'.format(NUM_PF))

#simul_perfから最大sharpe ratioをマークしたインデックスをilocおよびidxmax()を用いて取得
max_sharpe_idx = simul_perf.iloc[:,2].idxmax()
#iloc[max_sharpe_idx,:2]は二列目を含まないことに注意
sd, r = simul_perf.iloc[max_sharpe_idx, :2].values
print('Max Sharpe:{:.2%},{:.2%}'.format(sd,r))
⇒Max Sharpe:22.77%,34.30%

axes.scatter(x=sd,y=r, marker ='*', color = 'darkblue', s = 500, label = 'Max.Sharpe Ratio')

#simul_perfから最小リスクをマークしたインデックスをilocおよびidxmax()を用いて取得
min_vol_idx = simul_perf.iloc[:,0].idxmin()
sd,r = simul_perf.iloc[min_vol_idx,:2].values
axes.scatter(x=sd, y=r, marker ='*', color ='green', s = 500, label='Min Volatility')

plt.legend()

#最大シャープPFのパフォーマンスを評価するためにメソッドを定義する
#np.array(wt)@cov_values@np.array(wt)=variance of the PF
def portfolio_std(wt, rt=None, cov =None):
    """Annualized PF standard deviation"""
    return np.sqrt((np.array(wt)@cov.values@np.array(wt))*periods_per_year)

#(np.array(wt)@np.array(rt))+1=PFのリターン（これを投資額と掛ければ元金を含めたリターンが出せる）
def portfolio_returns(wt, rt=None):
    """Annualized  PF returns"""
    return ((np.array(wt)@np.array(rt))+1)


def portfolio_performance(wt,rt,cov):
    """Annualized PF returns & standard deviation"""
    r = portfolio_returns(wt, rt=rt)
    sd = portfolio_std(wt,cov=cov)
    return r,sd

#最大シャープPFを計算する関数を定義
def neg_sharpe_ratio(weights, mean_ret,cov):
    r,sd = portfolio_performance(weights, mean_ret, cov)
    return -(r-rf_rate)/sd

weights_constraint = {'type':'eq', 'fun':lambda x : np.sum(np.abs(x))-1}

#fun=最小化する関数、x0 = 最適化単探索の際に動かすパラメータ、args = パラメータ以外の入力値
#constraints bounds = パラメタへの制約
#options = 許容誤差と最大イテレーション回数
def max_sharpe_ratio(mean_ret, cov, short=False):
    return minimize(fun=neg_sharpe_ratio,
                   x0=x0,
                   args=(mean_ret,cov),
                   method='SLSQP',
                   bounds=((-1 if short else 0,1),)*n_assets,
                   constraints=weights_constraint,
                   options={'ftol':1e-10,'maxiter':1e4})

max_sharpe_pf = max_sharpe_ratio(mean_returns, cov_matrix, short=False)

#max_sharpe_pf.x=最適化された重みの配列
max_sharpe_perf = portfolio_performance(max_sharpe_pf.x, mean_returns, cov_matrix)
r, sd = max_sharpe_perf

#indexとdataのみ
pd.Series({'ret': r, 'sd': sd, 'sr': (r-rf_rate)/sd})

ret    1.002794
sd     0.138476
sr     7.238293
dtype: float64

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2297919/80bc61ed-cc9f-0cce-cb3c-1fbb87e65655.png)