Pythonで米国株ポートフォリオを評価・分析するコード『Analytics and Optimize Portfolio』【ポートフォリオ最適化】(楽天証券CSV)

4月 18, 2021

この記事は,楽天証券口座から取得した自分のポートフォリオのボラティリティ,期待リターンを計算し,どれくらい最適なのかを判定するPythonコードを提供します.

コードを実行すると,以下のような結果が表示されます.

Pythonを動かしたことがある人なら,コピペして実行するだけでできます

概要

楽天証券口座では自分が所持している銘柄と保有株式数が書かれたCSVファイルをダウンロードできるので,あとはこの記事で紹介するコードを実行すれば,自分のポートフォリオの成績が自動で計算されます

※計算された結果は,別のCSVファイルで保存されるようになっています.

もちろん,仕様を合わせれば自分で作成したエクセル(CSVファイル)でもこのコードは使えます.

また,紹介するコードは最適化計算されたポートフォリオも提供するので,自分のポートフォリオと見比べて改善することもできます.

また,コードが実行できない人でも可能なポートフォリオ最適化の方法を下で紹介するので,あきらめる必要はありません

※この記事で紹介するポートフォリオ最適化は将来の成績を保証するものではありません.未来は誰にも分かりませんから,どのようなポートフォリオにするのかは自分自身で判断してください.

使い方

まずは,簡単に手順を解説します.その後,より詳細に説明します.

自分のポートフォリオをダウンロード

自分の楽天証券口座にログインし,「保有商品」の「米国株」ページから『CSV形式で保存』をクリックして自分のポートフォリオをダウンロードします.

楽天証券口座(例)

ファイル名は,「data_us_rakuten.csv」で保存しておくと後々楽ちんです.

コードを保存

以下のコードをコピペして保存します.

コード『Analytics and Optimize Portfolio』

# import needed modules
import datetime
import fix_yahoo_finance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import csv

def get_portfolio_rakuten(mydata):
    port = mydata[["ティッカーコード", "保有数量[株]"]]
    del_list = []
    for i in range(len(port)):
        if port.iat[i,0] is np.nan:
            del_list.append(i)
    port.drop(port.index[[del_list]],inplace=True)
    return port
def get_tickers(port):
    tickers = []
    for i in range(0,len(port)):
        p = port.values.tolist()
        tickers.append(p[i][0])
    return tickers
def get_weights(port):
    weights = []
    for i in range(0,len(port)):
        p = port.values.tolist()
        weights.append(p[i][1])
    return weights
def add_waight(port, data, tickers):
    port["割合"] = 0.0
    sum = 0.0
    for i in range(len(port)):
        port.iat[i,2] = port.iat[i,1]*data.at[data.index[len(data)-1], tickers[i]]
        sum += port.iat[i,2]
    for i in range(len(port)):
        port.iat[i,2] /= sum
    port = port.loc[:, ["ティッカーコード", "割合", "保有数量[株]"]]
    return port
def opt_portfolio(port, selected):
    port.drop(port.index[[0,1,2]],inplace=True)
    port["ティッカー(仮)"] = selected
    port["保有数量[株]"] = np.nan
    port.columns=["割合", "ティッカーコード", "保有数量[株]"]
    port = port.loc[:, ["ティッカーコード", "割合", "保有数量[株]"]]
    for i in range(len(port)):
        port.rename(index={port.index[i]:str(i)}, inplace=True)
    return port
def calculate_performance(port, annual_data):
    portlist = port.values.tolist()
    weights = []
    for w in portlist:
        weights.append(w[1])
    weights = np.array(weights)
    #returns
    returns = np.dot(weights, annual_data[0])
    #volatility
    volatility = np.sqrt(np.dot(weights.T, np.dot(annual_data[1], weights)))
    #sharpe ratio
    sharpe = returns / volatility

    print('リターン' , returns)
    print('ボラティリティ' , volatility)
    print('シャープレシオ' , sharpe)
    return returns, volatility, sharpe
def make_the_risk_df(all_df, vola):
    v_df = all_df.copy()
    del_list = []
    for i in range(len(v_df)):
        if abs(v_df.iat[i,1]-vola) > 10**(-3):
            del_list.append(i)
    v_df.drop(v_df.index[[del_list]],inplace=True)
    return v_df
def make_the_return_df(all_df, ret):
    r_df = all_df.copy()
    del_list = []
    for i in range(len(r_df)):
        if abs(r_df.iat[i,0]-ret) > 10**(-3):
            del_list.append(i)
    r_df.drop(r_df.index[[del_list]],inplace=True)
    return r_df
def score(port, df):
    print('')
    print('My Portfolio のスコアを計算します')
    print('')
    mreturns, mvolatility, msharpe = calculate_performance(myport, annual_data)

    df_on_the_risk = make_the_risk_df(df, mvolatility)
    max_return_on_myport = df_on_the_risk['Returns'].max()
    min_return_on_myport = df_on_the_risk['Returns'].min()

    df_on_the_return = make_the_return_df(df, mreturns)
    min_volatility_on_myport = df_on_the_return['Volatility'].min()
    max_volatility_on_myport = df_on_the_return['Volatility'].max()

    max_sharpe = df['Sharpe Ratio'].max()
    min_sharpe = df['Sharpe Ratio'].min()

    score_return = (mreturns-min_return_on_myport)/(max_return_on_myport-min_return_on_myport)
    score_volatility = (mvolatility-max_volatility_on_myport)/(min_volatility_on_myport-max_volatility_on_myport)
    score_sharpe = (msharpe-min_sharpe)/(max_sharpe-min_sharpe)
    score = (0.4*score_return+0.4*score_volatility+0.2*score_sharpe)*100

    print('')
    print('なので,My Portfolio のスコアは' , int(score), '点です')
    print('')
    if score > 65:
        print('素晴らしいポートフォリオです!')
    elif 65 >= score > 50:
        print('まずまずですが,改善の余地があります.')
        if score_return > score_volatility:
            print('リスクを抑えましょう!')
        else:
            print('期待リターンを増やしましょう!')
    else:
        print('改善の余地があります!')

    return int(score)

#read csv data
mydata = pd.read_csv(filepath_or_buffer="data_us_rakuten.csv", encoding="ms932", sep=",")

myport = get_portfolio_rakuten(mydata)

#データ取得時間を設定
start = datetime.date(2016,1,1)
end = datetime.date.today()

#my portfolio tickers
selected = get_tickers(myport)

#get data
data = yf.download(selected, start=start, end=end)["Adj Close"]
data = data.reindex(columns=selected)

myport = add_waight(myport, data, selected)

# calculate daily and annual returns of the stocks
returns_daily = data.pct_change()
returns_annual = returns_daily.mean() * 250

# get daily and covariance of returns of the stock
cov_daily = returns_daily.cov()
cov_annual = cov_daily * 250

annual_data = [returns_annual, cov_annual]

# empty lists to store returns, volatility and weights of imiginary portfolios
port_returns = []
port_volatility = []
sharpe_ratio = []
stock_weights = []

# set the number of combinations for imaginary portfolios
num_assets = len(selected)
num_portfolios = 50000

#set random seed for reproduction's sake
np.random.seed(101)

# populate the empty lists with each portfolios returns,risk and weights
for single_portfolio in range(num_portfolios):
   weights = np.random.random(num_assets)
   weights /= np.sum(weights)
   returns = np.dot(weights, returns_annual)
   volatility = np.sqrt(np.dot(weights.T, np.dot(cov_annual, weights)))
   sharpe = returns / volatility
   sharpe_ratio.append(sharpe)
   port_returns.append(returns)
   port_volatility.append(volatility)
   stock_weights.append(weights)

# a dictionary for Returns and Risk values of each portfolio
portfolio = {'Returns': port_returns,
            'Volatility': port_volatility,
            'Sharpe Ratio': sharpe_ratio}

# extend original dictionary to accomodate each ticker and weight in the portfolio
for counter,symbol in enumerate(selected):
   portfolio[symbol] = [Weight[counter] for Weight in stock_weights]

# make a nice dataframe of the extended dictionary
df = pd.DataFrame(portfolio)

# get better labels for desired arrangement of columns
column_order = ['Returns', 'Volatility', 'Sharpe Ratio'] + [stock for stock in selected]

# reorder dataframe columns
df = df[column_order]

# find min Volatility & max sharpe values in the dataframe (df)
min_volatility = df['Volatility'].min()
max_sharpe = df['Sharpe Ratio'].max()

# use the min, max values to locate and create the two special portfolios
sharpe_portfolio = df.loc[df['Sharpe Ratio'] == max_sharpe]
min_variance_port = df.loc[df['Volatility'] == min_volatility]

print('My ポートフォリオ')
print(myport)
mreturns, mvolatility, msharpe = calculate_performance(myport, annual_data)
print('')
opt_portfolio1 = opt_portfolio(sharpe_portfolio.T, selected)
print('最適ポートフォリオ(シャープレシオ最大)')
print(opt_portfolio1)
returns1, volatility1, sharpe1 = calculate_performance(opt_portfolio1, annual_data)
print('')
opt_portfolio2 = opt_portfolio(min_variance_port.T, selected)
print('最適ポートフォリオ(ボラティリティ最小)')
print(opt_portfolio2)
returns2, volatility2, sharpe2 = calculate_performance(opt_portfolio2, annual_data)

rvs = [mreturns, mvolatility, msharpe, returns1, volatility1, sharpe1, returns2, volatility2, sharpe2]
#score of my portfolio
score = score(myport, df)

# CSVファイルとして出力
with open('portfolio.csv', 'w') as f:
    writer = csv.writer(f)
    writer.writerow(['My Portfolio','スコア',score,'','Optimize Portfolio(max Sharp Ratio)','','','','Optimize Portfolio(min Volatility)'])
    writer.writerow(['リターン', 'ボラティリティ' , 'シャープレシオ','', \
                    'リターン', 'ボラティリティ' , 'シャープレシオ','', \
                    'リターン', 'ボラティリティ' , 'シャープレシオ'])
    writer.writerow([mreturns, mvolatility, msharpe,'',returns1, volatility1, sharpe1,'',returns2, volatility2, sharpe2])
allportfolio = pd.concat([myport, opt_portfolio1, opt_portfolio2], axis=1)
allportfolio.to_csv('portfolio.csv', mode='a', header=False)

# plot frontier, max sharpe & min Volatility values with a scatterplot
plt.style.use('seaborn-dark')
df.plot.scatter(x='Volatility', y='Returns', c='Sharpe Ratio',
               cmap='RdYlGn', edgecolors='black', figsize=(8, 5), grid=True)
plt.scatter(x=sharpe_portfolio['Volatility'], y=sharpe_portfolio['Returns'], c='red', marker='s', s=200, label='sharpe ratio max')
plt.scatter(x=min_variance_port['Volatility'], y=min_variance_port['Returns'], c='blue', marker='s', s=200, label='volatility min' )
plt.scatter(x=mvolatility, y=mreturns, c='yellow', marker='s', s=200, label='my portfolio' )
# bboxの作成
boxdic = {
    "facecolor" : "white",
    "edgecolor" : "darkred",
    "boxstyle" : "Square",
    "linewidth" : 2
}
max_volatility = df['Volatility'].max()
plt.text(max_volatility, mreturns-0.01, "Sore " +str(score), ha='right', size=30, bbox=boxdic)
plt.xlabel('Volatility (Std. Deviation)')
plt.ylabel('Expected Returns')
plt.title('Efficient Frontier')
plt.legend(loc='upper left')
#plt.show()


fig_cir = plt.figure()
ax1 = fig_cir.add_subplot(1, 2, 1)
ax2 = fig_cir.add_subplot(1, 2, 2)
wm = get_weights(myport)
w1 = get_weights(opt_portfolio1)
w2 = get_weights(opt_portfolio2)
colors = []
color_list = ["r", "g", "b", "c", "m", "y", "k", "gray", "lightpink", "slateblue", "lightcoral", "orange", "springgreen", "Navy", "plum"]
for i in range(len(myport)):
    colors.append(color_list[i%len(color_list)])
ax1.pie(w1, labels=selected, counterclock=False, startangle=90, colors=colors)
ax1.pie(wm, counterclock=False, startangle=90, radius=0.7, colors=colors)
ax2.pie(w2, labels=selected, counterclock=False, startangle=90, colors=colors)
ax2.pie(wm, counterclock=False, startangle=90, radius=0.7, colors=colors)
circle1 = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.0)
circle2 = plt.Circle((0,0),0.4,color='white', fc='white',linewidth=1.0)
ax1.add_patch(circle1)
ax2.add_patch(circle2)
ax1.set_title("Maximize Sharpe Ratio", fontsize = 22)
ax2.set_title("Minimize Volatility", fontsize = 22)
plt.tight_layout()
plt.show()

保存は,先ほどのCSVファイル(自分のポートフォリオ)と同じ場所にしてください.

実行する

上のコードをPythonで実行すれば,以下のような結果が得られます.この結果の見方は,下で説明します.

結果1(画像)

画像1:効率的フロンティアとMy ポートフォリオ,最適ポートフォリオの位置
画像2:My ポートフォリオ(内側)と最適ポートフォリオ(外側)の株価割合(ドル)

結果2(ターミナル,CSVファイル)

My ポートフォリオ
   ティッカーコード        割合  保有数量[株]
0      AAPL  0.041279      1.0
1       VTI  0.068194      1.0
2        VT  0.032517      1.0
3       UAL  0.017365      1.0
4        VZ  0.019105      1.0
5       DAL  0.031374      2.0
6       VIG  0.142959      3.0
7       VYM  0.066820      2.0
8       HDV  0.031866      1.0
9      SPYV  0.012572      1.0
10     SPYD  0.013075      1.0
11      TSM  0.040652      1.0
12     PLTR  0.024197      3.0
13        U  0.031897      1.0
14      JNJ  0.053266      1.0
15       KO  0.173647     10.0
16      MRK  0.024993      1.0
17     MSFT  0.078975      1.0
18       PG  0.085101      2.0
19        T  0.010147      1.0
リターン 0.2782017391354812
ボラティリティ 0.19402098674695786
シャープレシオ 1.4338744679116175

最適ポートフォリオ(シャープレシオ最大)
   ティッカーコード        割合  保有数量[株]
0      AAPL  0.064400      NaN
1       VTI  0.030239      NaN
2        VT  0.003364      NaN
3       UAL  0.075595      NaN
4        VZ  0.047734      NaN
5       DAL  0.115061      NaN
6       VIG  0.001578      NaN
7       VYM  0.016491      NaN
8       HDV  0.142504      NaN
9      SPYV  0.042530      NaN
10     SPYD  0.015888      NaN
11      TSM  0.085574      NaN
12     PLTR  0.112376      NaN
13        U  0.111550      NaN
14      JNJ  0.016815      NaN
15       KO  0.030198      NaN
16      MRK  0.002761      NaN
17     MSFT  0.026707      NaN
18       PG  0.050880      NaN
19        T  0.007754      NaN
リターン 0.6236760324320775
ボラティリティ 0.25424001549591707
シャープレシオ 2.4530994116545486

最適ポートフォリオ(ボラティリティ最小)
   ティッカーコード        割合  保有数量[株]
0      AAPL  0.013162      NaN
1       VTI  0.002989      NaN
2        VT  0.082171      NaN
3       UAL  0.109163      NaN
4        VZ  0.114588      NaN
5       DAL  0.106454      NaN
6       VIG  0.000527      NaN
7       VYM  0.004089      NaN
8       HDV  0.003469      NaN
9      SPYV  0.011650      NaN
10     SPYD  0.025172      NaN
11      TSM  0.039098      NaN
12     PLTR  0.050606      NaN
13        U  0.043619      NaN
14      JNJ  0.016950      NaN
15       KO  0.047613      NaN
16      MRK  0.107388      NaN
17     MSFT  0.036366      NaN
18       PG  0.093601      NaN
19        T  0.091325      NaN
リターン 0.1768932355096627
ボラティリティ 0.16138643660518456
シャープレシオ 1.0960848955505098

My Portfolio のスコアを計算します

リターン 0.2782017391354812
ボラティリティ 0.19402098674695786
シャープレシオ 1.4338744679116175

なので,My Portfolio のスコアは 43 点です

改善の余地があります!

また,この情報は別の仕様で『portfolio.csv』というファイルに自動保存されますので,ファイルを覗いてみてください(画像は自動保存されませんから,自分で保存してください).

結果の見方

結果1(画像)

上の画像1は,設定した銘柄における効率的フロンティアを表しています.

各点は,各ポートフォリオを意味しています.横軸はボラティリティ,縦軸は期待年平均リターンです(100をかけると%).

黄色の四角の位置に,自分の「My ポートフォリオ」があります.

右に行くほどボラティリティが高く,リスクが高いと言えます.また,上に行くほど期待リターンが高いです.

つまり,なるべく左,なるべく上にある方が良いポートフォリオということになります.青色の四角はボラティリティが最も低くなるようなポートフォリオです.

また,各点の色は緑であるほどシャープレシオが大きく,赤色であるほどシャープレシオが小さいです.

シャープレシオとは,リターンをボラティリティで割った値のことで,シャープレシオが大きいほどリスクに対するリターンが高く,バランスが良いポートフォリオという見方ができます.赤色の四角はシャープレシオが最大になる点です.

また,画像2は具体的なポートフォリオを示しています.

左の円グラフはシャープレシオを最大にするポートフォリオ(外側)と自分のポートフォリオ(内側)で,

右の円グラフはボラティリティを最小にするポートフォリオ(外側)と自分のポートフォリオ(内側)です.

円グラフの割合は株価ベース(ドル)です(株式数ではありません).

結果2(ターミナル,CSVファイル)

文字で見える結果としては,

(1)シャープレシオが最大になるポートフォリオ(赤い四角)

(2)ボラティリティが最小になるポートフォリオ(青い四角)

が,具体的にどのようなポートフォリオ(各銘柄の割合)になっているかが分かります.

これを参考にすることで,自分のポートフォリオを良い方向に改善できる可能性があります.

また,自分のポートフォリオの『スコア』が100点満点で表示されます.

スコアは,ボラティリティと期待リターン,シャープレシオのそれぞれが,考えられるポートフォリオの中での理想値にどれだけ近いかを計算して算出しています.

スコアの算出方法は私の独自の方法ですので,参考程度にしてください.

スコアの算出方法については,以下の記事で説明しています.

コードが実行できなくてもっと簡単にポートフォリオは最適化できる

上を読んで,「実行できそうにないな…」と思った人も,ポートフォリオ最適化をあきらめる必要はありません.

なぜなら,銘柄を打ち込むだけで,最適なポートフォリオを提案してくれるツールがネット上にあるからです.

たとえば,PORTFOLIO VISUALIZERは所持している(もしくは所持する予定の)米国株銘柄を入力するだけで,以下のように最適化されたポートフォリオを表示してくれます.

PORTFOLIO VISUALIZER

また,今回紹介したコードの結果をウェブ上の手入力で動かせるツールOptPFも以下のリンクから利用することができます.

これでも十分有益な情報は得られますから,自分のポートフォリオ改善に役立ててください.

コード解説

全ては解説しませんが,重要な部分は説明しておきます

コードを実行するために必要なこと

コードが実行できない人向けです.これを見ても分からない場合は,残念ですが上のコードを使うのはあきらめて下さい.

Pythonのインストール

プログラミング言語「Python」をパソコンにダウンロード,インストールしてください.Pythonは無料でインストールできる言語です.以下の記事を参考にインストールしてください.

ライブラリのインストール

コードを実行するには,いくつかのライブラリが必要です.

ライブラリは,ターミナル(Windowsだとコマンドプロンプト)で

pip install ライブラリ名

とすればインストールできます.

例えば,今回はデータの取得にはYahoo!ファイナンスからデータを取得できるPythonライブラリ『fix_yahoo_finance』を用いていますから,

pip install fix_yahoo_finance

でインストールします.

他のライブラリも同様です.今回使用しているライブラリは,コードの一番上に書かれています(datetime,fix_yahoo_finance,pandas,numpy,matplotlib.pyplot,csv).

ターミナルでプログラムを実行する

pythonコードを実行するには,コードが保存されているディレクトリ(階層)で,以下のコマンドを実行します.

python ファイル名.py

CSVファイルを別の名前で保存した場合

楽天証券かCSVファイルの名前を「data_us_rakuten.csv」以外にしたひとは,コードの120行目

mydata = pd.read_csv(filepath_or_buffer="data_us_rakuten.csv", encoding="ms932", sep=",")

の部分を書き換えてください.

データ取得期間について

取得するデータの期間を変更するには,コードの124~126行目の

#データ取得時間を設定
start = datetime.date(2016,1,1)
end = datetime.date.today()

を書き換えてください.

そもそもポートフォリオ最適化は中長期に向いているので,データは少なすぎると良い結果が得られませんし,昔すぎるデータを入れても現実と乖離しますからあまりよくありません.

個人的には,10年くらいで見るのが良いかと思います.

ただし,最近上場した銘柄を含んだポートフォリオである場合は,上場後から設定する必要があると思います.

コードについてのより詳細な解説は,別の記事でしようと思います.

最適な株式数について

本コードは最適なポートフォリオ(各銘柄の割合)を表示しますが,実際には株式は1株単位でしか買えないことがほとんどなので,購入株式数を考えたときに計算上の最適ポートフォリオに近づけようとしてもズレが生じます.

そのズレをできるだけ少なく購入株式数を決定したいわけですが,そのような問題は『組合せ最適化問題』と言われ,少々専門的なアルゴリズムが必要です.

そのアルゴリズムについては今後考え,最適な購入株式数を決定できるツールを開発する予定です

よって現時点では,得られた最適ポートフォリオの割合に近くなるように,自ら株式数を調節する必要があります(上の表示される結果では,『NaN』になってます).

投資に関する免責事項
プログラムや考え方の情報の提供・作業代行を目的としており,投資勧誘を目的とするものではありません.また,この記事で紹介するポートフォリオ最適化は将来の成績を保証するものではありません.未来は誰にも分かりませんから,どのようなポートフォリオにするのかは自分自身で判断してください.また,コードの正確性には一切責任は負いませんので,不備がある場合はご自身で修正願います.

この記事に関してご質問等ありましたら,下のコメント欄にいただけると幸いです.