3ファクターモデルをPythonで実装して期待リターンを求める
3ファクターモデルとは
CAPMを発展させた理論です。
CAPMは、市場のリスクプレミアムのみで期待リターンを求めています。
#CAPM 個別株の期待リターン = β * (市場リスクプレミアム - 無リスク金利) + 無リスク金利
CAPMはとてもシンプルで金融市場の値動きの本質を表しているのですが、シンプルゆえときに値動きを説明できないタイミングあります。
そのため、もう少し変数を加えて細かく金融市場の値動きを説明してみよう、というのが3ファクターモデルです。
名前の通り、ファクター(変数)が3つです。
- 市場リスクプレミアム
- 小型株ファクター(SMB)
- サイズファクター(HML)
それぞれのファクターのβを計算し、個別株の期待リターンを求めます。
3ファクターモデルの計算式
個別株の期待リターン = β * 市場リスクプレミアム + SMB'β * SMB + HML'β * HML + 無リスク金利
CAPMにでてこないファクターであるSMBとHMLについて説明します。
SMBとは
SMB : Small minus Big
小型株のパフォーマンスと大型株のパフォーマンスの差を表しています。
多くの分析から、小型株は大型株よりも高いリターンを出すことがわかっています。
小型株を保有したときの、期待リターンをSMBという指標によって考慮します。
SMBは時価総額によって、企業を2つに分類します。
企業規模(Small size,Big size)
HMLとは
HML : High minus Low
簿価時価比率が高い銘柄と低い銘柄のパフォーマンスの差です。
簿価時価比率 = 簿価 / 時価
簿価時価比率が高ければ、簿価が評価されておらず、時価がついてきていないということになります。
そのため、会社の価値はあるが市場が高い値をつけていないバリュー株ということになります。
簿価時価比率は、PBRの逆数で求めることができます。
PBR = 時価 / 一株当たり純資産
これの逆数なので...
簿価時価比率 = 一株当たり純資産 / 時価
一株当たり純資産は、帳簿上の解散価値です。つまり、1株あたりのの帳簿上の価値を表しています。
そのため、簿価として一株当たり純資産を使うってことですね。(たぶん)
簿価時価比率によって企業を3つに分類します。
簿価時価比率(Low BM,Medium BM,High BM)
SMBとHMLを計算式
企業規模(Small size,Big size)と簿価時価比率(Low BM,Medium BM,High BM)の組み合わせで,次の6つのポートフォリオに分割します。
簿価時価比率(低) | 簿価時価比率(中) | 簿価時価比率(大) | |
時価総額(小) | (1).Small size-Low BM(S/LB) | (2).Small size-Medium BM(S/MB) | (3).Small size-High BM(S/HB) |
時価総額(大) | (4).Big size-Low BM(B/LB) | (5).Big size-Medium BM(B/MB) | (6).Big size-High BM(B/HB) |
SMB =( (1) + (2) + (3) )/3 - ( (4) + (5) + (6) )/3 HML =( (3) + (6) )/2-( (1) + (4) )/2,
このSMBとHMLは市場全ての企業を対象に計算します。
日本市場であれば、市場全ての企業 = 東証1部 が適切かな、と思います。
これで3ファクターモデルの式に出てくる変数が全て出揃いました。
個別株の期待リターン = β * 市場リスクプレミアム + SMB'β * SMB + HML'β * HML + 無リスク金利
実際は、以下の線形回帰式を最小二乗法でそれぞれのファクターのベータを求めます。
y = ax + a2x2 + a3x3 + b
Pythonで実装
注意!!ここから適当なところあります
3ファクターモデルを計算するには、過去の株価だけではなく、時価総額と時価簿価比率のデータが必要です。
集めてくるのがとっても面倒です。
なので、アメリカの市場のリスクプレミアムとSMLとHMLのデータがあるので、それを使うことにしました...笑
以下のgithubのソースコードで使われているデータを使用しています。
期待リターンを求める企業だけが日本企業というちぐはぐっぷりですが...
理論と実装方法がメインの記事なので気にしません。
以下から市場リスクプレミアム、HML、SMLのデータを取得します。
http://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_Factors_CSV.zip
データを扱いやすい形に整形します。
データは2000年以降のものにフィルタリングします。
import pandas as pd import numpy as np import datetime import os, sys corrent_dir = os.getcwd() os.chdir("../") sys.path.append(os.getcwd()) import scraping_stock os.chdir(corrent_dir) FFdata = pd.read_csv("F-F_Research_Data_Factors.CSV", header = 0, names = ["Date","MKT-RF","SMB","HML","RF"], skiprows=3) FFdata = FFdata[:1074] for i in range(len(FFdata)): d = FFdata["Date"].iloc[i] FFdata["Date"].iloc[i] = datetime.datetime(int(d[0:4]), int(d[4:6]), 1, 0, 0, 0, 0)# print(FFdata.head(10)) # print(FFdata.tail(10)) FFdata = FFdata.loc[FFdata["Date"] >= datetime.datetime(2000, 1, 1, 0, 0, 0, 0)] FFdata = FFdata.sort_values("Date", ascending=True)
上記のデータが2000年01月から2015年12月までなので、同じ期間のNTTデータ(9613)の株価を取得します。
株価取得のコードはMPTを実装したときのコードを参考にしてください。
ここでは、2000年01月から2015年12月の期間のデータが既にあるとして、データの整形にはいります。
stock = 9613 # scraping_stock.scraping(stock, days=5475 , term="m") df = pd.read_csv("../data/code_{0}.csv".format(stock)) df = df.dropna(how="any") # Datetime形式に変換 date_format = "%Y年%m月" for i in range(len(df)): df["Date"].iloc[i] = datetime.datetime.strptime(df["Date"].iloc[i], date_format) df = df.sort_values("Date", ascending=True) # 必要なデータを切り出し tmp_df = df.loc[:, ["Date", "Adj Close"]] # リターンを計算する # tmp_df["Return"] = np.log(tmp_df["Adj Close"]) - np.log(tmp_df["Adj Close"].shift(1)) tmp_df["Return"] = tmp_df["Adj Close"].pct_change()
2つのデータを1つのDataFrameにまとめます。
data = pd.merge(FFdata, tmp_df, on="Date") data.index = data["Date"].values data = data.drop("Date", axis=1) data = data.astype(float) data = data.dropna(how="any") print(data.head()) # リターンから無リスク金利をひきます data["XReturn"] = data["Return"] * 100 - data["RF"].astype(float)
3ファクターモデルに基づいて、期待リターンを計算していきます。
それぞれのファクターのβは、60ヶ月分のデータから求めます。
個別株の期待リターン(60ヶ月分) = β * 市場リスクプレミアム(60ヶ月分) + SMB'β * SMB(60ヶ月分) + HML'β * HML(60ヶ月分) + 無リスク金利
求めたβを使って、個別株の期待リターンを求めます。
市場のリスクプレミアムとSMBとHMLは、持ちうるデータの最大期間を使って個別株の期待リターンを求めます。
個別株の期待リターン = β * 市場リスクプレミアム(最大期間) + SMB'β * SMB(最大期間) + HML'β * HML(最大期間) + 無リスク金利
import statsmodels.api as sm # 60ヶ月 t = 60 tf_list = [] # 2000 ~ 2015年まで計算 for i in range(11, len(data)-t, 12): # 最大期間を取得 MKT_RF = data.iloc[0:i+t, [0]].mean().values[0]#"MKT-RF" SMB = data.iloc[0:i+t, [1]].mean().values[0]#"SMB" HML = data.iloc[0:i+t, [2]].mean().values[0]#"HML" RF = data.iloc[i+t, [3]].values[0] print("MKT-RF : ", MKT_RF, "SMB : ", SMB, "HML : ", HML) # 60ヶ月分 tmp_data = data.ix[i:i+t] print(tmp_data) # 3ファクターモデルを計算 y = tmp_data["XReturn"] X = tmp_data.loc[:, ["MKT-RF", "SMB", "HML"]] X = sm.add_constant(X) model = sm.OLS(y, X) results = model.fit() # print(results.summary()) alpha, beta, smb_beta, hml_beta = results.params print(alpha, beta, smb_beta, hml_beta) # 求めたβで期待リターンを計算 expected_return = (beta * MKT_RF) + (smb_beta * SMB) + (hml_beta * HML) + alpha + RF # 求めた期待リターンは、月間でもデータなので年率に変換 print(expected_return * 12)
いちおう結果
2006 ~ 2015年の期間でNTTデータの期待リターンを3ファクターモデルで求め、実際のリターンを比較しました。
3ファクターモデルで求めた期待リターン
年 | 期待リターン |
---|---|
2006 | 3.380415% |
2007 | 15.638408% |
2008 | 19.402995% |
2009 | 3.269230% |
2010 | 3.621298% |
2011 | -10.057815% |
2012 | -9.765555% |
2013 | -6.118449% |
2014 | 5.041712% |
2015 | 11.430143% |
実際のNTTデータの期待リターン
年 | 期待リターン |
---|---|
2006 | 3.189316966221089% |
2007 | -15.716405993233739% |
2008 | -27.606051376861046% |
2009 | -14.094971477913244% |
2010 | 0.8188833324962229% |
2011 | -10.280445385445681% |
2012 | 13.43885732894585% |
2013 | 39.04595333427926% |
2014 | 17.567901291557355% |
2015 | 27.7547208081126% |
ちなみにアメリカの市場のリスクプレミアム、SMB、HMLを使っているで本当に結果に意味はありません。
考察するに値しないので、結果としてはこういった形で出る、というくらいです。
参考
計算式はこの論文を参考にしています。
http://www2.itc.kansai-u.ac.jp/~koji_ota/Paper/PublishedPaper/Ota_2012_Shogakuronshu.pdf
データはこのgithubのコードに使われているものを参考にしています。
https://github.com/nakulnayyar/FF3Factor/blob/master/FamaFrench3Factor.ipynb
個別株と市場のボラティリティの関係性を示すβをPythonで計算する
βとは
βは、CAPMというポートフォリオ理論に出てくる概念であり、個別株と市場のボラティリティの関係性を示します。
日本経済新聞のサイトでは、銘柄のβランキングが公表されています。
個別株とは、TOYOTAであったりNTTデータであったり、ひとつひとつの会社のことです。
市場とは、株式市場全体のことです。日本では、TOPIX = 市場 として扱われることが多いと思います。
βは高ければ、高いほど市場より高いボラティリティがあり、低ければ低いほど市場より低いボラティリティということになります。
ボラティリティ = リスク であるため、言い換えるとβが高い株は、リスクの高い株、ということにもなります。
β = 1.0 : 株式が市場と同じボラティリティ β = 2.0 : 株式が市場より高いボラティリティ β = 0.5 : 株式が市場より低いボラティリティ
TOYOTAとさくらネットの株価の日々のリターンをビジュアル化してみます。
それぞれのβは以下になります。
銘柄名 | β |
---|---|
さくらネット(3778) | 1.54 |
TOYOTA(7203) | 1.02 |
βがさくらネットのほうが高く、ボラティリティが高いことがわかると思います。
言うても、0.5ほどしか差がないので、微妙な差ですが....
βの求め方
β = 個別株と市場ポートフォリオの相関 × 個別株の標準偏差 / 市場ポートフォリオの標準偏差
この式が何をしているかと言うと...
個別株の値動きのうち、市場と連動している部分のみをβとしています。
個別株の値動きは、大きく2つにわけることができます。
1つは、市場の動き。もう1つは、個別株特有の動きです。
TOPIXが上昇すれば、個別株もそれに応じて上昇します。
これが市場の動きです。
逆に個別株特有の動きとは、決算の結果が良かったや、粉飾決算がバレた、などです。
これらのイベントによって個別株は、特有の値動きをするので、この値動きは市場の動きとは別です。
市場の動きと個別株特有の動きが合わさって、個別株の値動きになります。
個別株の値動きの中から個別株特有の動きを控除したものを、個別株のβとしているのです。
どうやって個別株特有の動きを控除するかと言うと、個別株と市場の相関係数を使います。
個別株と市場ポートフォリオの相関 × 個別株の標準偏差
個別株と市場の値動きの相関係数が0.7だった場合は、0.7は市場の動き、残りの0.3は個別株特有の動きであると。
そういった解釈になります。
Pythonで実装する
株価を取得するプログラミングは、現代ポートフォリオ理論をPythonで実装したときと同じものを利用しるので割愛します。
βの計算対象は、さくらネット(3778)にします。
市場は、TOPIX(998405)にしています。
とりあえず、pandasのDataFrameでさくらネットとTOPIXを統合したものを作成します。
Close_998405 Close_3778 1800.44 850 1783.49 852 1778.87 843 1744.01 811 1761.71 816 1763.76 817 1759.65 823 1771.13 831 1777.08 839 1780.56 820 1776.73 831 1772.07 816 1786.15 820 1792.08 833 1792.66 854
上記のデータが master_df に入っています。
returns_daily = master_df.pct_change() # β = 証券iと市場ポートフォリオの相関 × 証券i の標準偏差 / 市場ポートフォリオの標準偏差 tmp_df = returns_daily.loc[:, ["Close_998405", "Close_3778"]] # 相関係数を計算 corr = tmp_df.corr().iloc[0, 1] # それぞれの標準偏差を計算 std_i = returns_daily["Close_3778"].std() std_m = returns_daily["Close_998405"].std() # βの計算 beta = std_i * corr / std_m # print("beta : ", beta)
プログラミング全体は、gitlabにおいています。
上のコードでは、愚直にpandasで式を再現しています。
scipyの単回帰でもβを求めることができますし、こっちのほうがコードが短くてスマートです。
tmp_df = returns_daily.loc[:, ["Close_998405", "Close_3774"]] x = [i[0] for i in tmp_df.iloc[:, [0]].values] y = [i[0] for i in tmp_df.iloc[:, [1]].values] slope, intercept, r_value, p_value, std_err = stats.linregress(x[1:len(x)], y[1:len(y)]) print("β : ", slope)
計算の結果は、どちらも以下になります。
β : 1.54465802366
計算期間は、2017/11/04 ~ 2018/07/12にしています。
まとめ
特になし!
以上おわり!
現代ポートフォリオ理論(MPT)をPythonで実装しよう
はじめに ~ 現代ポートフォリオ理論って何? ~
Modern Portfolio Theory(MPT) = 現代ポートフォリオ理論。
ハリー・マーコウィッツというおじさんが提唱した理論で、ノーベル賞を受賞しています。
ファイナンス理論の下敷きというか、基礎でとても大事な考え方!
この記事は、現代ポートフォリオ理論をpythonで実装します。
理論とともに、リアルワールドの株価データを使ってビジュアライズしながら見きましょう。
今日つかうコードはgitlabにおいています。
現代ポートフォリオ理論の仮定
現代ポートフォリオ理論は、いくつかの仮定の上に成り立っています。
- 投資家は合理的であり、できるだけリスクを避ける
- 投資家はリターンを最大化することが目的である
- 投資家は期待リターンを最大化する
- 手数料と税は、とりあえず考えない
- 投資家は全ての情報を取得することができ、取得できる情報のレベルも同じである
- 投資家は、無限に無利子でお金を借りる
これらの仮定を理解した上で、現代ポートフォリオ理論とは...
投資家がポートフォリオを決めるときに、取れるリスクの中で最大のリターンの期待値を求める理論です。
現代ポートフォリオ理論の目の付け所がシャープだったのは、リスクとリターンの関係がポートフォリオにどう影響を与えているか、を分析した点です。
リターンをとればリスクが増える、というのは真実です。(ハイリスク・ハイリターン)
しかし、同じリスクでも期待できるリターンには差があります。
現代ポートフォリオ理論によって計算されたリターンとリスクの関係は、ビジュアライズすることができます。
2つの銘柄を組み合わせてポートフォリオを作成し、2つの銘柄の比率を変化させていきます。
比率を変化させていくごとに現代ポートフォリオ理論に則って、リターンとリスクを計算していくと、こんなグラフができあがります。
X軸がリスク、Y軸がリターンを表しています。
これを効率的フロンティアと呼びます。
投資家が許容できるリスクの中で最大のリターンを期待できるポイントがこのグラフからひと目でわかります。
この青部分と赤部分のリスクは同じです。
しかし、期待できるリターンには差があります。
これが同じリスクでも期待できるリターンには差がある、と言った意味です。
2つの銘柄の組み入れる比率を変えることで、期待できるリターンをコントロールすることができます。
これが現代ポートフォリオ理論において一番大事なことです。
Pythonで実装しよう
2つ以上の株価データを用意しましょう。
2つのデータは時系列で、かつ、終値があれば良しです。
pandasのDataFrame形式で扱います。
Unnamed: 0 Date Open High Low Close Volume Adj Close 24 4 2018年6月4日 1234 1246 1233 1244 2091700 1244 23 3 2018年6月5日 1253 1267 1251 1266 3545600 1266 22 2 2018年6月6日 1264 1268 1258 1266 2834800 1266 21 1 2018年6月7日 1270 1279 1262 1276 3662600 1276 20 0 2018年6月8日 1276 1287 1266 1280 5072300 1280 4 4 2018年7月2日 1275 1283 1248 1251 2375800 1251 3 3 2018年7月3日 1255 1262 1226 1241 3435500 1241 2 2 2018年7月4日 1230 1270 1228 1265 2100400 1265 1 1 2018年7月5日 1269 1281 1250 1262 3467900 1262 0 0 2018年7月6日 1266 1267 1240 1248 3424000 1248
基本的には関数は使わず、べた書きでコードは貼っていきますが、株価データの取得だけはべた書きだと冗長になるので関数化しておきます。
import pandas as pd import numpy as np import matplotlib import datetime import time matplotlib.use("Agg") import matplotlib.pyplot as plt def scraping(stock, days): end = datetime.date.today() start = end - datetime.timedelta(days=days) base = "http://info.finance.yahoo.co.jp/history/?code={0}.T&{1}&{2}&tm={3}&p={4}" start_g = str(start) start_g = start_g.split("-") start_g = "sy={0}&sm={1}&sd={2}".format(start_g[0], start_g[1], start_g[2]) end = str(end) end = end.split("-") end = "ey={0}&em={1}&ed={2}".format(end[0], end[1], end[2]) page = 1 term = "d" #term = "m" result = [] while True: url = base.format(stock, start_g, end, term, page) # print(url) try: df = pd.read_html(url, header=0) except ValueError: break if len(df[1]) == 0: break result.append(df[1]) page += 1 time.sleep(1.0) try: result = pd.concat(result) result.columns = ["Date", "Open", "High", "Low", "Close", "Volume", "Adj Close"] yahoo_df = pd.DataFrame(result) yahoo_df.to_csv("./data/code_" + str(stock) + ".csv", encoding="utf-8") except ValueError: pass
NTTデータ(9613)とSCSK(9719)のデータを使います。
今日(2018年7月7日)から180営業日前までのデータを取得します。
リターンとリスクを計算するときは、私はできるだけ長期間のデータを使用するべきだと考えています。
その理由は、長期間でなければその銘柄の本当のリターンとリスクが見えないからです。
短期的には経済など外部要因が大きく株価に影響を与えることは多いですから。
このコードは、スクレイピングした株価データをdataというフォルダに格納するので、dataフォルダを作成します。
stock_list = [9613, 9719] days = 180 for s in stock_list: scraping(s, days)
株価データをconcatして、一つのDataFrameにします。
master_df = pd.DataFrame({}) for s in stock_list: df = pd.read_csv("./data/code_{0}.csv".format(s)) df = df.sort_values("Date", ascending=True) print(df.tail(10)) tmp_df = df.loc[:, ["Date", "Adj Close"]] tmp_df.columns = ["Date_{0}".format(s), "Close_{0}".format(s)] master_df = pd.concat([master_df, tmp_df["Close_{0}".format(s)]], axis=1)
データの加工は続きます。
リターンと共分散を計算します。
# Calculate annual average return # 年率を計算する前に日率を計算 # .pct_change:Rate of change # リターンを計算する便利な関数 print(master_df.tail(10)) returns_daily = master_df.pct_change() print(returns_daily.tail(10)) # Convert to annual rate. working day is 250/365. # 年率に変換. 250は年間の市場が動いている日数. returns_annual = returns_daily.mean() * 250 print("returns_annual") print(returns_annual) # Calculate covariance # 各々のリターンから共分散を計算 cov_daily = returns_daily.cov() cov_annual = cov_daily * 250 # print("cov_annual") # print(cov_annual)
DataFrameの加工は終了です。
さて、効率的フロンティアをえがきましょう!
NTTデータ(9613)とSCSK(9719)を様々な組み合わせ(50000パターン)でリターン・リスクを計算してきます。
※"Sharpe Ratio"の項目は今はムシしてください。後で使います。
# Define list of returns, volatilities, ratiosm, sharpe ratio # 諸々のリスト定義 port_returns = [] port_volatility = [] stock_weights = [] sharpe_ratio = [] # Choose a prime number. # seedは素数を選ぼう np.random.seed(101) # Number of stocks to be combined # 組み入れる株の数 num_assets = len(stock_list) # Number of trials of portfolio creation pattern # ポートフォリオ作成パターンの試行回数 num_portfolios = 50000 # Randomly calculate portfolio risks and returns # 様々な銘柄の比率でのポートフォリオのリターンとリスクを計算 for single_portfolio in range(num_portfolios): # Determination of the ratio of stocks by random number # 銘柄の比率を乱数で決定 weights = np.random.random(num_assets) weights /= np.sum(weights) # Calculate expected return on portfolio # ポートフォリオの期待リターンを計算 returns = np.dot(weights, returns_annual) # print("returns : ", returns) # Calculate portfolio volatility # ポートフォリオのボラティリティを計算 volatility = np.sqrt(np.dot(weights.T, np.dot(cov_annual, weights))) # print("volatility : ", volatility) # Calculate sharp ratio sharpe = returns / volatility sharpe_ratio.append(sharpe) # Store calculated values in list # 計算値をリストに格納 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} # Add ratio data # 計算したポートフォリオのリターンとリスクに、比率のデータを加える for counter,symbol in enumerate(stock_list): portfolio[str(symbol) + " Weight"] = [Weight[counter] for Weight in stock_weights] # Convert to DataFrame # PandasのDataFrameに変換 df = pd.DataFrame(portfolio) # Done # データフレーム完成 column_order = ["Returns", "Volatility", "Sharpe Ratio"] + [str(stock)+" Weight" for stock in stock_list] df = df[column_order] print(df.head(10))
print(df.head(10)) で計算された様々なポートフォリオのパターンが格納されています。
seabornで可視化しましょう。
# look an efficient frontier # 50000パターンのポートフォリオを可視化して、効率的フロンティアを探る plt.style.use("seaborn") df.plot.scatter(x="Volatility", y="Returns", c="Sharpe Ratio", cmap="RdYlGn", edgecolors="black", figsize=(10, 8), grid=True) plt.xlim([0,1]) plt.xlabel("Volatility (Std. Deviation)") plt.ylabel("Expected Returns") plt.title("Efficient Frontier") plt.savefig("image.png")
あまりきれいなグラフではありません。
なぜならNTTデータ(9613)とSCSK(9719)が同じ業種で同じような株価データだからです。
効率的フロンティアは、相関係数が低いほどきれいなカーブがえがけます。
SoftBank(9984)とTIS(3626)に入れ替えて、もう一度計算してみましょう。
stock_list = [9984, 3626]
NTTデータ(9613)とSCSK(9719)の過去180営業日の相関係数は、0.866926です。
対し、SoftBank(9984)とTIS(3626)は、-0.380903です。
3つ以上の銘柄で効率的フロンティアをえがく
TIS(3626), NTTデータ(9613), Yahoo(4689), SoftBank(9984)でポートフォリオをつくってみましょう。
stock_list = [3626, 9613, 4689, 9984]
もっとも効率のいいポートフォリオはどの組み合わせ?
効率のいいポートフォリオを見つけるためにシャープ・レシオを使います。
Sharp Ratio = (Expected return - Risk-free rate of return) / Standard deviation シャープ・レシオ = (期待リターン - 無リスク資産) / 標準偏差
無リスク資産とは、債券などをさしていてい、今回はポートフォリオに入れていないので、考慮なしです。
そのため、実質の計算式は、
シャープ・レシオ = 期待リターン / 標準偏差
となります。
# Calculate sharp ratio
sharpe = returns / volatility
sharpe_ratio.append(sharpe)
この数値、さきほどポートフォリオを50000パターンの計算していたコードの中に既にあります。
DataFrameの中にも"Sharp Ratio"というカラムはあります。
そのため、最終的なアウトプットであるDataFrameをシャープ・レシオでソートすると、最も効率のいいポートフォリオを導くことができます。
df = df.sort_values("Sharpe Ratio", ascending=True) print(df.tail(1)) # Returns Volatility Sharpe Ratio 3626 Weight 9613 Weight \ # 0.691151 0.618261 1.117896 0.871933 0.008536 # 4689 Weight 9984 Weight # 0.002589 0.116942
シャープ・レシオが1.117で、
資産の87.19%をTIS(3626)、0.8%をNTTデータ(9613)、0.2%をYahoo(4689)、11.69%をSoftBank(9984)に振り分けるポートフォリオが一番最適であるとなりました。
過去180営業日しかとっていないので、TIS買っとけ、みたいな分析結果になってしまいました。
ちなみに、シャープ・レシオに色をつけて、可視化するともっとわかりやすくなります。
df.plot.scatter(x="Volatility", y="Returns", c="Sharpe Ratio", cmap="RdYlGn", edgecolors="black", figsize=(10, 8), grid=True) # df.plot.scatter(x='Volatility', y='Returns', figsize=(10, 8), grid=True)
まとめ
久々にちゃんと記事書きました。褒めて。
以上、終わり!
非対称な収益率
収益率の計算方法についての話。(時間も時間なので最低限でまとめます)
収益率の計算は、だいたい2種類あります。
- 収益率(ノーマル)
- 対数収益率
よく使われる収益率は、これ↓
ほげほげ銘柄を2018/7/2、100円で買いました。
10日間持っていると、200円になりました。
このときのリターンは、
200円 / 100円 = 2.0 = 200%
この収益率の計算方法がめじゃーではあるのですが、問題点が一つあります。
それは非対称なところです。
例えば、ほげほげ銘柄がこんな値動きをしていたとします。
日付 | 2018/07/02 | 2018/07/03 | 2018/07/04 | 2018/07/05 | 2018/07/06 | 2018/07/07 |
ほげほげ銘柄 | 100円 | 99円 | 90円 | 100円 | 110円 | 100円 |
2018/07/02 ~ 2018/07/07まで、毎日ほげほげ銘柄を売買したとします。
100円で買い、次の日に99円で売る。 -1円の損失
その日のうちに99円で買い、次の日に90円で売る。 - 9円の損失
その日のうちに90円で買い、次の日に100円で売る。 +10円の利益
その日のうちに100円で買い、次の日に110円で売る。 +10円の利益
その日のうちに110円で買い、次の日に100円で売る。 -10円の損失
2018/07/07には、この取引の利益と損失はプラスマイナスゼロになります。
しかし、これを収益率で表すとそうはならないです。これが非対称性のことやねん。
収益率の計算式は、↓これです↓。
今日の株価 / 前日の株価
日付 | 2018/07/02 | 2018/07/03 | 2018/07/04 | 2018/07/05 | 2018/07/06 | 2018/07/07 |
ほげほげ銘柄 | 100円 | 99円 | 90円 | 100円 | 110円 | 100円 |
収益率 | - | 0.99 | 0.9090909090909091 | 1.1111111111111112 | 1.1 | 0.9090909090909091 |
この収益率をトータルすると、+1.003858585858586となります。
%に変換すると+0.3%になります。
取引の収益は、ゼロなのに収益率にするとプラスになってしまい、これが非対称性と呼ばれています。
そのため、収益率を計算するときは対数に変換して、収益率は計算されます。
日付 | 2018/07/02 | 2018/07/03 | 2018/07/04 | 2018/07/05 | 2018/07/06 | 2018/07/07 |
ほげほげ銘柄 | 100円 | 99円 | 90円 | 100円 | 110円 | 100円 |
ほげほげ銘柄(対数) | 4.605170185988092 | 4.59511985013459 | 4.499809670330265 | 4.605170185988092 | 4.700480365792417 | 4.605170185988092 |
収益率 | - | 0.99 | 0.9090909090909091 | 1.1111111111111112 | 1.1 | 0.9090909090909091 |
対数で計算をするので、計算式は↓これです↓。
今日の株価(対数) - 前日の株価(対数)
日付 | 2018/07/02 | 2018/07/03 | 2018/07/04 | 2018/07/05 | 2018/07/06 | 2018/07/07 |
ほげほげ銘柄 | 100円 | 99円 | 90円 | 100円 | 110円 | 100円 |
ほげほげ銘柄(対数) | 4.605170185988092 | 4.59511985013459 | 4.499809670330265 | 4.605170185988092 | 4.700480365792417 | 4.605170185988092 |
収益率 | - | 0.99 | 0.9090909090909091 | 1.1111111111111112 | 1.1 | 0.9090909090909091 |
対数収益率 | -0.010050335853502013 | -0.09531017980432477 | 0.10536051565782678 | 0.09531017980432477 | -0.09531017980432477 |
対数収益率はトータルしても、プラスマイナスゼロとなり、取引の結果と一致します。
まとめ
収益率の計算は、必ず対数収益率を使いましょう。
検証に使ったコードです。
import math import numpy as np # logに変換 def calc_log(num): log = [math.log(i) for i in num] return log # 対数収益率を計算 def calc_return(log): r_list = [log[i+1] - log[i] for i in range(len(log)-1)] print(r_list) r = sum(r_list) / len(r_list) return r # 収益率を計算 def calc_return_int_ver(log): r_list = [log[i+1] / log[i] for i in range(len(log)-1)] print(r_list) r = sum(r_list) / len(r_list) return r # データセット x = [100, 99, 90, 100, 110, 100] # logに変換したデータセット log_x = calc_log(x) print("log(x)") print(log_x) # 収益率 return_x_int_ver = calc_return_int_ver(x) print("x return int ver : ", return_x_int_ver) # 対数収益率 return_x = calc_return(log_x) print("x return : ", return_x)
seleniumの2つの待機方法
seleniumの待機方法でsleep.time()を使っている人は、この記事を読んで、心を清めてほしい。
seleniumには、2つの待機方法がある。
- Explicit Waits(明示的な待機)
- Implicit Waits(暗黙的な待機)
この2つの待機方法の使い分けは、こんな感じ。
- Explicit Waits = 複雑な条件で待機させたい
- Implicit Waits = 単純な待機
Implicit Waitsは、要素が出現するまで待機する、という設定になる。
そのため、ごくごく単純なスクレイピング、例えば静的サイトなどであればImplicit Waitsで十分である。
しかし、非同期のWebサイトであればImplicit Waitsでは対応できないことが多い。
例えば、スクロールしてくことでページが読み込まれていくサイトのスクレイピングなど。
非同期のWebサイトからスクレイピングするときは、「このログインボタンがクリックできるようになるまで待つ」などの条件を指定したほうが、エラーは出にくいコードが書ける。
ま、使い分けと言いつつ、実際に使うときはImplicit Waitsに10秒くらい指定して、各ポイントでExplicit Waitsに30秒とかを指定することが多い。
implicitly_waitの使い方
driver = webdriver.Chrome()
driver.implicitly_wait(20)
これだけ。
とても簡単。書くのは1回だけ。
デフォルトは、0が設定されている。
上の例では、要素が読み込まれるまで20秒待機している。
例えば、Webページの読み込むスピードが単純に遅いときは、コードが実行された時点で要素が現れておらず、エラーとなることがある。
selenium.common.exceptions.ElementNotVisibleException: Message: element not visible: Element is not currently visible and may not be manipulated
seleniumを使っていれば、このエラーに100%エンカウントしているはず。
その時は、implicitly_wait(20)の待機時間を充分な時間を指定して、ページが読み込まれるのを待てばいい。
ただ、implicitly_waitは基本的に一律の設定である。
この処理は何秒待つ、とかの指定はできない。
できないことはないけど、絶対に人に見せたくないコードになる。
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
driver.implicitly_wait(5) driver.find_element_by_id("mul_t").send_keys("ログインしたい") driver.implicitly_wait(20) driver.find_element_by_xpath('//*[@id="control_object_class1"]/div/div[3]/div[1]/div').click() driver.implicitly_wait(10) driver.find_element_by_name("forward_sch").click()
Explicit Waitsの使い方
Explicit Waitsは複雑な条件を指定して、条件が満たされるまで待機するという設定ができる。
この例は、"sch"というIDを持つ要素がクリックできるようになるまで最大30待機する、という指示。
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By WebDriverWait(driver, 30).until(EC.element_to_be_clickable((By.ID, "sch"))) # 探している要素↓ よくある検索ボタンのhtmlだと思う。 # <input type="button" value="検索" id="sch">
seleniumの公式ページにものっているし、定番はこれ。
IDが"myDynamicElement"の要素が読み込まれるまで待機。使いやすい。
WebDriverWait(driver, 30).until(EC.presence_of_element_located((By.ID, "myDynamicElement"))
presence_of_element_located以外にも指定できる条件はある。
title_is title_contains presence_of_element_located visibility_of_element_located visibility_of presence_of_all_elements_located text_to_be_present_in_element text_to_be_present_in_element_value frame_to_be_available_and_switch_to_it invisibility_of_element_located element_to_be_clickable staleness_of element_to_be_selected element_located_to_be_selected element_selection_state_to_be element_located_selection_state_to_be alert_is_present
また、By.はID以外も指定できる。
ID XPATH LINK_TEXT PARTIAL_LINK_TEXT NAME TAG_NAME CLASS_NAME CSS_SELECTOR
まとめ
大抵のサイトは、非同期技術が使ってあるため、requestsでリクエストをWebサイトに送って、返ってきたレスポンスをbeautifulsoup4で読み込む、という定番のスクレイピング方法では対応できないことが多い。
ただ、seleniumでも非同期技術を考慮なしに、スクレイピングをしても、とても不安定なコードになってしまう。
面倒ではあるが、ある程度は考えて待機時間は設定したほうが、後々のメンテも楽になるので、がんばりましょう。
最後にtime.sleep()で待機させてから、スクレイピングをしている方へ。
time.sleep()は、要素が読み込まれていようといまいと待機することになるので、時間がもったいないからやめよう。
以上、終わり!!
seleniumで起きるエラー ~unknown error: unable to discover open pages~
発生エラー
selenium.common.exceptions.WebDriverException: Message: unknown error: unable to discover open pages
発生原因
stackoverflowにナイスな回答あり!
chromedriverのバージョン古くないか? ← まずこれを疑え
次は、google-chromeのバージョン古くないか? ← これも疑え
とのこと。
簡単な解決方法は、バージョンアップしよう、ってことになる。
バージョンアップしよう
chromedriver編
バージョンごとにインデックスされているので、目的のバージョンをインストールしよう。
http://chromedriver.storage.googleapis.com/index.html
最新版のバージョンはここから確認可能。
http://chromedriver.storage.googleapis.com/LATEST_RELEASE
2018/06/23時点での最新版は2.40なので、2.40をインストールする。
$ wget https://chromedriver.storage.googleapis.com/2.40/chromedriver_linux64.zip $ unzip chromedriver_linux64.zip -d ~/bin/
起動するとバージョンが出てくるので、インストールできているかを確認する。
$ chromedriver
google-chrome編
$ wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb $ sudo dpkg -i google-chrome-stable_current_amd64.deb $ sudo apt-get update $ sudo apt-get grade
最新版がインストールできているかを確認する。
$ google-chrome --version
最新版は、このサイトから確認できる。
https://www.whatismybrowser.com/guides/the-latest-version/chrome
なぜバージョンアップで解決できる?
seleniumがブラウザを自動制御するときには以下の構成になっている。
selenium >> chromedriver >> google-chrome
seleniumは直接は、ブラウザ(google-chrome)は制御できないので、chromedriverを介して制御する。
ユーザーによっては、googl-chromeはFireFoxだったり。
このエラーは、chromedriverとgoogle-chromeのどちらかが古いことで起きる。
chromedriverのバージョンが古すぎると、google-chomeを制御できなし、その逆もしかり。
chrome-dirverには、サポートしているchromedriverのバージョンの幅も調べたら出てくるので、ちゃんと確認したければ、そこを確認する。
これでもまだ解決しないとき
chromedriverをダウングレードすると解決することが多い。
これに関しては、なぜかは不明。。。
現在最新版の2.40をインストールしてもエラーが解決できなかったため、順にダウングレードしていって2.38版でエラーなく動作するようになった。
$ wget https://chromedriver.storage.googleapis.com/2.39/chromedriver_linux64.zip ↓↓↓↓↓↓ $ wget https://chromedriver.storage.googleapis.com/2.38/chromedriver_linux64.zip
chromedriverは結構頻繁にバージョンが更新されているようで、こまめにバージョンを気にしないとバグのもとに...
フォントサイズをレスポンシブ対応させるCSS
@media を使うと、CSSに条件を追加することができる。
これで状況によってCSSを変更することが可能に。
例えば、ブラウザのサイズによってフォントのサイズを変えたい時とか。つまり、レスポンシブ対応。
@mediaの使い方
@mediaのあとに条件を指定する。
なんとなく文法がjavascriptっぽい。
@media (min-width: 1200px) { html { font-size: 11px } }
ブラウザのサイズが1200px以上であれば、フォントサイズを11pxに指定している。
CSS書いてみる
ブラウザはだいたい3タイプくらいを想定。
条件は以下に設定。
100px以上のブラウザサイズ:font-size:8px
768px以上のブラウザサイズ:font-size:10px
1200px以上のブラウザサイズ:font-size:11px
@media (min-width: 100px) { html { font-size: 8px } } @media (min-width: 768px) { html { font-size: 10px } } @media (min-width: 1200px) { html { font-size: 11px } }