ストックドッグ

KatoTakahiro。金融系の会社で働くSEが株やPython、その他諸々について書いています。サービスも運営してます→http://fmbrain.work

現代ポートフォリオ理論(MPT)をPythonで実装しよう

はじめに ~ 現代ポートフォリオ理論って何? ~

Modern Portfolio Theory(MPT) = 現代ポートフォリオ理論。

ハリー・マーコウィッツというおじさんが提唱した理論で、ノーベル賞を受賞しています。

ファイナンス理論の下敷きというか、基礎でとても大事な考え方!


この記事は、現代ポートフォリオ理論をpythonで実装します。

理論とともに、リアルワールドの株価データを使ってビジュアライズしながら見きましょう。


今日つかうコードはgitlabにおいています。

gitlab.com

現代ポートフォリオ理論の仮定

現代ポートフォリオ理論は、いくつかの仮定の上に成り立っています。

  • 投資家は合理的であり、できるだけリスクを避ける
  • 投資家はリターンを最大化することが目的である
  • 投資家は期待リターンを最大化する
  • 手数料と税は、とりあえず考えない
  • 投資家は全ての情報を取得することができ、取得できる情報のレベルも同じである
  • 投資家は、無限に無利子でお金を借りる


これらの仮定を理解した上で、現代ポートフォリオ理論とは...


投資家がポートフォリオを決めるときに、取れるリスクの中で最大のリターンの期待値を求める理論です。


現代ポートフォリオ理論の目の付け所がシャープだったのは、リスクとリターンの関係がポートフォリオにどう影響を与えているか、を分析した点です。

リターンをとればリスクが増える、というのは真実です。(ハイリスク・ハイリターン)

しかし、同じリスクでも期待できるリターンには差があります。


現代ポートフォリオ理論によって計算されたリターンとリスクの関係は、ビジュアライズすることができます。

2つの銘柄を組み合わせてポートフォリオを作成し、2つの銘柄の比率を変化させていきます。

比率を変化させていくごとに現代ポートフォリオ理論に則って、リターンとリスクを計算していくと、こんなグラフができあがります。

X軸がリスク、Y軸がリターンを表しています。

f:id:doz13189:20180707174856p:plain

これを効率的フロンティアと呼びます。

投資家が許容できるリスクの中で最大のリターンを期待できるポイントがこのグラフからひと目でわかります。

f:id:doz13189:20180707175236p:plain

この青部分と赤部分のリスクは同じです。

しかし、期待できるリターンには差があります。

これが同じリスクでも期待できるリターンには差がある、と言った意味です。


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

f:id:doz13189:20180707175352p:plain

あまりきれいなグラフではありません。

なぜならNTTデータ(9613)とSCSK(9719)が同じ業種で同じような株価データだからです。

効率的フロンティアは、相関係数が低いほどきれいなカーブがえがけます。

SoftBank(9984)とTIS(3626)に入れ替えて、もう一度計算してみましょう。

stock_list = [9984, 3626]

f:id:doz13189:20180707174856p:plain

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]

f:id:doz13189:20180707175448p:plain

もっとも効率のいいポートフォリオはどの組み合わせ?

効率のいいポートフォリオを見つけるためにシャープ・レシオを使います。

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)

f:id:doz13189:20180707175546p:plain

まとめ

久々にちゃんと記事書きました。褒めて。

以上、終わり!