ポートフォリオ最適化『Analytics and Optimize Portfolio』Pythonコード詳細解説

3月 22, 2021

このページでは,以下の記事で紹介した米国株ポートフォリオを最適化するPythonコード『Analytics and Optimize Portfolio』の解説を行います.

この記事(コード)を見ていない方は,まずは以下のリンクからご覧ください.

このページの想定読者は,このポートフォリオ最適化コードを応用したい,プログラミングを勉強したい人です.

コード解説

具体的に,解説を行う全コードは以下です.

コード『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()

上から順番に説明していきます.

import

# 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

使用するPythonライブラリをインポートしています.pipなどでインストールしておいて下さい.

datetime,fix_yahoo_finance,pandasは株価のデータ取得に使います.

pandasとnumpyはデータの加工や計算に使用します.

matplotlib.pyplotはデータのグラフ化に使います.

csvはCSVファイルからデータを取得したり,データを書き込んだ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

楽天証券からダウンロードしたCSVファイルのデータ(pandasのDataFrame)を,ティッカーコードと保有株数の情報だけのDataFrameに変換します.

def get_tickers(port):
    tickers = []
    for i in range(0,len(port)):
        p = port.values.tolist()
        tickers.append(p[i][0])
    return tickers

DataFrameから,ティッカーコードだけをリストとして返します.

def get_weights(port):
    weights = []
    for i in range(0,len(port)):
        p = port.values.tolist()
        weights.append(p[i][1])
    return weights

DataFrameから,ポートフォリオの割合だけをリストとして返します.円グラフの表示にしか使いません.

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

get_portfolio_rakutenで取得した自分のポートフォリオDataFrameに,割合のカラムを追加します.

これによって,"ティッカーコード", “割合", “保有数量[株]"のカラムを持ったDataFrame(自分のポートフォリオ)が得られます.

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

計算した最適ポートフォリオのDataFarameを,自分のポートフォリオのDataFarameと同じ形にします.

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

ポートフォリオのリターン,ボラティリティ,シャープレシオを計算します.annual_dataは事前に計算した各銘柄の年平均リターン,共分散(ボラティリティ)の情報が入っています.

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

用意した全てのポートフォリオの可能性の中で,与えたボラティリティ付近の(色々なリターンの)データだけを返します.スコアの計算に使います.

ef 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)

ポートフォリオのスコアを計算する関数です.

スコアの計算方法はこの関数を見るか,以下の記事をご覧ください.

ポートフォリオ最適化コード『Analytics and Optimize Portfolio』のスコアの算出方法について

メインコード

自分のポートフォリオを取得

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

楽天証券からダウンロードした(このコードと同じディレクトリにある)CSVファイルを,pandasのDataFarameの形でmydataとして保存します.

また,そのデータをget_portfolio_rakuten関数で加工し,ティッカーコードと保有株数の情報だけのDataFrameに変換します.

データ取得時間を設定

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

ティッカーコードのリストを取得

#my portfolio tickers
selected = get_tickers(myport)

後々頻繁に使うので,ティッカーコードだけを情報として持つリストをselectedとして持ちます.

データを取得

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

fix_yahoo_finance(as yf)を使って,選んだselected(自分の持つ保有銘柄のティッカーコード)に関するデータをDataFarameとして取得します.[“Adj Close"]なので,株価の終値だけを持ってきています.

また,fix_yahoo_financeだとティッカーコードによってカラムの順番が変わってしまうので,data.reindex(columns=selected)でselectedの順番に戻しています.今後のデータ加工を簡単にするためです.

自分のポートフォリオDataFarameに株価割合カラムを追加

myport = add_waight(myport, data, selected)

株価の情報(data)が手に入ったので,それを元に自分のポートフォリオDataFarameに株価割合カラムを追加します.

これで,"ティッカーコード", “割合", “保有数量[株]"の情報を持ったDataFrame(自分のポートフォリオ)が得られたことになります.

年平均リターン,(共)分散を計算

# 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]

returns_dailyは,各銘柄の前日に対する株価の変動率で,これが指定したdataの日数分あります.つまり,(dataの日数)×(銘柄数)のDataFarameです.

returns_annualは各銘柄の年平均リターンです.returns_dailyの平均,つまり1日あたりの変動率の平均に一年間の市場日数250日をかけて計算しています.1×(銘柄数)のDataFarameです.

cov_dailyは,returns_dailyの共分散です.(銘柄数)×(銘柄数)のDataFarameです.cov_annualは年平均分散です.

最後に,コードの都合でannual_dataとしてreturns_annual, cov_annualを保存しています(calculate_performance関数でポートフォリオのリターン,ボラティリティ,シャープレシオを計算するときに使います).

ポートフォリオデータのための準備

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

本コードの以降では,たくさんのポートフォリオをランダムで取得します.その各ポートフォリオのリターン,ボラティリティ,シャープレシオ,割合を保存するためのリストを用意しています.

また,num_assetsは銘柄数,num_portfoliosはランダムで生成するポートフォリオの数です.

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)

先ほどのnum_portfoliosで指定した数の分(ここでは50000個)だけ,ランダムでポートフォリオを生成します.

ランダムでweights(各銘柄の割合)を決め,weights /= np.sum(weights)で合計が1になるように規格化しています.

ポートフォリオの期待リターンは,各銘柄の割合に各銘柄の年平均リターンをかけて和を取ります(np.dot(weights, returns_annual)).

ポートフォリオのボラティリティは,np.sqrt(np.dot(weights.T, np.dot(cov_annual, weights)))で計算した偏差です.割合で共分散行列を挟んで平方根をとる,いわゆる行列計算で偏差を出す式です.

シャープレシオは,returns / volatilityで計算した,よいポートフォリオを意味する値です.

最後に,各ポートフォリオのデータを保存するためにリストに格納します.

これで,50000個のポートフォリオのデータを作成することができました.

ポートフォリオデータをDataFarameに変換

# 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]

先ほど生成した各ポートフォリオデータを,DataFarameに格納しています.

ポートフォリオデータから,最小のボラティリティと最大のシャープレシオを取得

# 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]

calculate_performance関数によって,自分のポートフォリオ,シャープレシオが最大になるポートフォリオ,ボラティリティが最小になるポートフォリオの期待リターン,ボラティリティ,シャープレシオを計算して出力します.

自分のポートフォリオ以外は二度手間なので,先ほどのDataFarameから取り出した方が速いとは思いますが,コードの見た目の都合でこのようにしてしまいました.

最後に,rvsに成績データを格納しています.グラフをプロットするときに使います.

自分のポートフォリオのスコアを計算

#score of my portfolio
score = score(myport, df)

CSVファイルにデータを書き込み

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

with open('portfolio.csv’, 'w’) as fでportfolio.csvをいうファイルを書き込みモードで開き,成績データおよびポートフォリオを書き込みしています.

pd.concatはDataFarameを結合するものですが,キレイに結合されておらず,出力CSVファイルは見にくい状況なので,この辺は改良しなければなりません.単に私のサボりです.

グラフを作って表示

# 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()

あとは,matplotlibを使って円グラフと効率的フロンティアのプロットグラフを作るだけです.

この辺は単純なプロットなので,省略します.

最後に,コード全体を再掲しておきます.

コード『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()