用Python分析多股票的投资组合
俗话说不要将所有的鸡蛋放在同一个篮子里,在投资股票的时候我们也会多买几只以抵抗风险。本文将带领着你使用Python,来分析多只股票投资时的收益和风险,并找到最优的投资组合方案。这是上一篇文章《用Python分析股票的收益和风险》 的多股票升级版本。
本文目录如下:
一、股票数据的在线获取
我们打算用以下9家大公司的股票构建投资组合,并用2017年的历史数据进行回溯测试。
公司名 | 股票代码 |
---|---|
Apple | AAPL |
Microsoft | MSFT |
Exxon Mobil | XOM |
Johnson & Johnson | JNJ |
JP Morgan | JPM |
Amazon | AMZN |
General Electric | GE |
FB | |
AT&T | T |
在具体分析前,还是先导入将要用到的Python包。
import pandas as pd
import numpy as np
import quandl # 获取股票数据
from datetime import date
import matplotlib.pyplot as plt
%config InlineBackend.figure_format = 'retina'
我们使用上面导入的 quandl 包从网络获取相应的股票数据,并将每日调整后的收盘价存入数据框 StockPrices
变量中,具体获取方法可参见《如何用Python下载金融数据》一文。
# 创建空的DataFrame变量,用于存储股票数据
StockPrices = pd.DataFrame()
# 设置股票数据的开始和结束的时间
start = date(2016,12,30)
end = date(2017,12,31)
# 创建股票代码的列表
ticker_list = ['AAPL', 'MSFT', 'XOM', 'JNJ', 'JPM', 'AMZN', 'GE', 'FB', 'T']
# 使用循环,挨个获取每只股票的数据,并存储调整后的收盘价
for ticker in ticker_list:
data = quandl.get('WIKI/'+ticker, start_date=start, end_date=end)
StockPrices[ticker] = data['Adj. Close'] # 注意 Adj. 和 Close 之间有一空格
# 输出数据的前5行
print(StockPrices.head())
AAPL MSFT XOM JNJ JPM AMZN \
Date
2016-12-30 114.389454 60.788710 86.960273 112.310940 84.383167 749.87
2017-01-03 114.715378 61.219142 87.567241 112.925087 85.302395 753.67
2017-01-04 114.586983 60.945231 86.603799 112.739868 85.458859 757.18
2017-01-05 115.169696 60.945231 85.312787 113.919421 84.672217 780.45
2017-01-06 116.453639 61.473488 85.264615 113.373512 84.682050 795.99
GE FB T
Date
2016-12-30 30.782801 115.05 40.476170
2017-01-03 30.870473 116.86 40.942507
2017-01-04 30.880215 118.69 40.704580
2017-01-05 30.704870 120.67 40.590375
2017-01-06 30.792542 123.41 39.790940
这里用行来记录每一天的数据,用列记录每只股票的收盘价。然后计算每天的收益率,将数据存储在数据框 StockReturns
变量中。收益率的具体计算方法可参见文章《用Python分析股票的收益和风险》。
# 计算每日收益率,并丢弃缺失值
StockReturns = StockPrices.pct_change().dropna()
# 打印前5行数据
print(StockReturns.head())
AAPL MSFT XOM JNJ JPM AMZN \
Date
2017-01-03 0.002849 0.007081 0.006980 0.005468 0.010893 0.005068
2017-01-04 -0.001119 -0.004474 -0.011002 -0.001640 0.001834 0.004657
2017-01-05 0.005085 0.000000 -0.014907 0.010463 -0.009205 0.030732
2017-01-06 0.011148 0.008668 -0.000565 -0.004792 0.000116 0.019912
2017-01-09 0.009160 -0.003183 -0.016497 -0.000172 0.000697 0.001168
GE FB T
Date
2017-01-03 0.002848 0.015732 0.011521
2017-01-04 0.000316 0.015660 -0.005811
2017-01-05 -0.005678 0.016682 -0.002806
2017-01-06 0.002855 0.022707 -0.019695
2017-01-09 -0.004745 0.012074 -0.012585
至此,我们已经准备好了用于分析的数据 StockReturns
, 它记录了9只股票2017年每天的收益率。如果你想跳过以上步骤直接开始分析的话,也可以下载我为你准备好的数据(点我哦!),再用以下代码读取。
# 从CSV文件读取数据
StockReturns = pd.read_csv('StockReturns2017.csv', parse_dates=['Date'], index_col='Date')
二、投资组合的收益计算
我们选了9只股票,可资金怎么分配呢?哪只买多些,哪只买少些?这就需要对它们设置相应的权重,下面我们采用三种权重分配的方案,来计算不同组合下的投资收益。
2.1 给定权重的投资组合
第一种方案是预先设置一组权重,如下所示,注意所有股票权重的和为1。
公司名 | 股票代码 | 权重 |
---|---|---|
Apple | AAPL | 12% |
Microsoft | MSFT | 15% |
Exxon Mobil | XOM | 8% |
Johnson & Johnson | JNJ | 5% |
JP Morgan | JPM | 9% |
Amazon | AMZN | 10% |
General Electric | GE | 11% |
FB | 14% | |
AT&T | T | 16% |
我们将每只股票的收益,乘上其对应的权重,得到加权后的股票收益;再对所有股票加权后的收益求和,得到该组合投资的收益。
# 设置组合权重,存储为numpy数组类型
portfolio_weights = np.array([0.12, 0.15, 0.08, 0.05, 0.09, 0.10, 0.11, 0.14, 0.16])
# 将收益率数据拷贝到新的变量 stock_return 中,这是为了后续调用的方便
stock_return = StockReturns.copy()
# 计算加权的股票收益
WeightedReturns = stock_return.mul(portfolio_weights, axis=1)
# 计算投资组合的收益
StockReturns['Portfolio'] = WeightedReturns.sum(axis=1)
# 绘制组合收益随时间变化的图
StockReturns.Portfolio.plot()
plt.show()
以上绘制了该组合投资收益随时间变化的图,显得有些凌乱,因为画的是每天的收益。如果把每天的收益进行累积,可以绘制如下常见的收益曲线。
# 计算累积的组合收益,并绘图
CumulativeReturns = ((1+StockReturns["Portfolio"]).cumprod()-1)
CumulativeReturns.plot()
plt.show()
因为后面我们会不断绘制这样的累积收益曲线,所以将绘制的代码写成函数 cumulative_returns_plot()
,方便后续调用。
# 累积收益曲线绘制函数
def cumulative_returns_plot(name_list):
for name in name_list:
CumulativeReturns = ((1+StockReturns[name]).cumprod()-1)
CumulativeReturns.plot(label=name)
plt.legend()
plt.show()
2.2 等权重的投资组合
第二种方案是平均分配每只股票的权重,使它们都相等。这是最简单的投资方法,可作为其他投资组合的参考基准。计算方法和上面一致,只需更改存储权重的数组。
# 设置投资组合中股票的数目
numstocks = 9
# 平均分配每一项的权重
portfolio_weights_ew = np.repeat(1/numstocks, numstocks)
# 计算等权重组合的收益
StockReturns['Portfolio_EW'] = stock_return.mul(portfolio_weights_ew, axis=1) \
.sum(axis=1)
# 绘制累积收益曲线
cumulative_returns_plot(['Portfolio', 'Portfolio_EW'])
上图中蓝色曲线代表第一种方案的累积收益,橙色曲线代表等权重组合的累积收益,它们俩比较接近,说明第一种方案设置的权重并没有多少优势,我们还需要寻找更好的投资组合。
1.3 市值加权的投资组合
第三种法案是考虑了公司的市值,按市值的占比来分配权重。因此市值高的公司对应的权重就更大,当这些大公司的股票表现良好时,该投资组合的表现也更好。众所周知的标普500指数就是按照市值进行加权计算的。
以下列出了这9家公司2017年1月前的市值(单位:10亿美元)。
公司名 | 股票代码 | 市值 |
---|---|---|
Apple | AAPL | 601.51 |
Microsoft | MSFT | 469.25 |
Exxon Mobil | XOM | 349.5 |
Johnson & Johnson | JNJ | 310.48 |
JP Morgan | JPM | 299.77 |
Amazon | AMZN | 356.94 |
General Electric | GE | 268.88 |
FB | 331.57 | |
AT&T | T | 246.09 |
# 创建市值的数组
market_capitalizations = np.array([601.51, 469.25, 349.5, 310.48, 299.77,
356.94, 268.88, 331.57, 246.09])
# 计算市值权重
mcap_weights = market_capitalizations / np.sum(market_capitalizations)
# 计算市值加权的组合收益
StockReturns['Portfolio_MCap'] = stock_return.mul(mcap_weights, axis=1).sum(axis=1)
cumulative_returns_plot(['Portfolio', 'Portfolio_EW', 'Portfolio_MCap'])
上图中绿色曲线代表市值加权的组合投资,这里它的表现明显优于前两种方案。
三、相关性和协方差
3.1 相关矩阵
相关矩阵用于估算多只股票收益之间的线性关系,可使用pandas数据框内建的 .corr()
方法来计算。
# 计算相关矩阵
correlation_matrix = stock_return.corr()
# 输出相关矩阵
print(correlation_matrix)
AAPL MSFT XOM JNJ JPM AMZN GE \
AAPL 1.000000 0.436104 0.063337 0.030635 0.197596 0.506639 0.002801
MSFT 0.436104 1.000000 0.085906 0.227071 0.195753 0.621833 -0.044486
XOM 0.063337 0.085906 1.000000 0.132549 0.300892 0.021084 0.180453
JNJ 0.030635 0.227071 0.132549 1.000000 0.074452 0.048620 0.068477
JPM 0.197596 0.195753 0.300892 0.074452 1.000000 0.014245 0.260649
AMZN 0.506639 0.621833 0.021084 0.048620 0.014245 1.000000 -0.094063
GE 0.002801 -0.044486 0.180453 0.068477 0.260649 -0.094063 1.000000
FB 0.542663 0.549501 -0.048242 0.078149 0.093780 0.653162 -0.021957
T 0.004983 -0.019970 0.194238 0.095602 0.242841 -0.014819 0.284551
FB T
AAPL 0.542663 0.004983
MSFT 0.549501 -0.019970
XOM -0.048242 0.194238
JNJ 0.078149 0.095602
JPM 0.093780 0.242841
AMZN 0.653162 -0.014819
GE -0.021957 0.284551
FB 1.000000 -0.029623
T -0.029623 1.000000
矩阵中每个一元素都是其对应股票的相关系数,取值从-1到1,正数代表正相关,反之负数代表负相关。我们观察到矩阵的对角线永远是1,因为自己和自己当然是完全相关的。另外相关矩阵也是对称的,即上三角和下三角呈镜像对称。
为了便于观察,可以将数值的相关矩阵用热图的形式展现出来。以下采用了 seaborn 包来绘制热图。
# 导入seaborn
import seaborn as sns
# 创建热图
sns.heatmap(correlation_matrix,
annot=True,
cmap="YlGnBu",
linewidths=0.3,
annot_kws={"size": 8})
plt.xticks(rotation=90)
plt.yticks(rotation=0)
plt.show()
3.2 协方差矩阵
相关系数只反了股票之间的线性关系,但并不能告诉我们股票的波动情况,而协方差矩阵则包含这一信息。可使用pandas数据框内建的 .cov()
方法来计算协方差矩阵。
# 计算协方差矩阵
cov_mat = stock_return.cov()
# 年化协方差矩阵
cov_mat_annual = cov_mat * 252
# 输出协方差矩阵
print(cov_mat_annual)
AAPL MSFT XOM JNJ JPM AMZN GE \
AAPL 0.031577 0.011494 0.001268 0.000622 0.005721 0.018938 0.000099
MSFT 0.011494 0.022000 0.001435 0.003850 0.004731 0.019401 -0.001313
XOM 0.001268 0.001435 0.012688 0.001707 0.005522 0.000500 0.004045
JNJ 0.000622 0.003850 0.001707 0.013067 0.001387 0.001169 0.001558
JPM 0.005721 0.004731 0.005522 0.001387 0.026546 0.000488 0.008451
AMZN 0.018938 0.019401 0.000500 0.001169 0.000488 0.044248 -0.003937
GE 0.000099 -0.001313 0.004045 0.001558 0.008451 -0.003937 0.039601
FB 0.016428 0.013885 -0.000926 0.001522 0.002603 0.023406 -0.000744
T 0.000152 -0.000508 0.003755 0.001876 0.006791 -0.000535 0.009719
FB T
AAPL 0.016428 0.000152
MSFT 0.013885 -0.000508
XOM -0.000926 0.003755
JNJ 0.001522 0.001876
JPM 0.002603 0.006791
AMZN 0.023406 -0.000535
GE -0.000744 0.009719
FB 0.029021 -0.000866
T -0.000866 0.029461
3.3 投资组合的标准差
投资组合的风险可以用标准差来衡量,只要知道组合权重和协方差矩阵,就可以通过以下公式进行计算。
- :投资组合的标准差
- :收益的协方差矩阵
- :投资组合的权重(是权重的转置)
- 是点积运算
在NumPy中,使用.T
属性对数组进行转置,np.dot()
函数用于计算两个数组的点积。
# 计算投资组合的标准差
portfolio_volatility = np.sqrt(np.dot(portfolio_weights.T,
np.dot(cov_mat_annual, portfolio_weights)))
print(portfolio_volatility)
0.0896350886377703
四、寻找最优的投资组合
掌握了收益和风险(标准差)的计算方法后,接下来要考虑的是:应该选择怎样的组合权重才是最好的呢?是让收益最大吗?还是风险最小?我们需要综合权衡风险和收益这两个因素。
诺贝尔经济学奖得主马科维茨(Markowitz)提出的投资组合理论被广泛用于组合选择和资产配置中。该理论中的均值-方差分析法和有效边界模型可用于寻找最优的投资组合。
4.1 蒙特卡洛模拟Markowitz模型
我们采用蒙特卡洛模拟来进行分析,也就是随机生成一组权重,计算该组合下的收益和标准差,重复这一过程许多次(比如1万次),将每一种组合的收益和标准差绘制成散点图。
# 设置模拟的次数
number = 10000
# 设置空的numpy数组,用于存储每次模拟得到的权重、收益率和标准差
random_p = np.empty((number, 11))
# 设置随机数种子,这里是为了结果可重复
np.random.seed(123)
# 循环模拟10000次随机的投资组合
for i in range(number):
# 生成9个随机数,并归一化,得到一组随机的权重数据
random9 = np.random.random(9)
random_weight = random9 / np.sum(random9)
# 计算年化平均收益率
mean_return = stock_return.mul(random_weight, axis=1).sum(axis=1).mean()
annual_return = (1 + mean_return)**252 - 1
# 计算年化的标准差,也称为波动率
random_volatility = np.sqrt(np.dot(random_weight.T,
np.dot(cov_mat_annual, random_weight)))
# 将上面生成的权重,和计算得到的收益率、标准差存入数组random_p中
random_p[i][:9] = random_weight
random_p[i][9] = annual_return
random_p[i][10] = random_volatility
# 将numpy数组转化成DataFrame数据框
RandomPortfolios = pd.DataFrame(random_p)
# 设置数据框RandomPortfolios每一列的名称
RandomPortfolios.columns = [ticker + "_weight" for ticker in ticker_list] \
+ ['Returns', 'Volatility']
# 绘制散点图
RandomPortfolios.plot('Volatility', 'Returns', kind='scatter', alpha=0.3)
plt.show()
投资的本质是在风险和收益之间做出选择,上图正是刻画了这两个要素。其中每一个点都代表着一种投资组合的情况,横坐标是代表风险的标准差,纵坐标是收益率。
Markowitz投资组合理论认为,理性的投资者总是在给定风险水平下对期望收益进行最大化,或者是在给定收益水平下对期望风险做最小化。反映在图中也就是红色曲线所示的有效边界,只有在有效边界上的点才是最有效的投资组合。
现在我们知道,理性的投资者都会选择有效边界上的投资组合。可具体选择哪个点呢?我们接着往下看。
4.2 风险最小组合
一种策略是选择最低的风险,且在该风险水平下收益最高的组合,称为最小风险组合(GMV portfolio)。
让我们找到风险最小的组合,并绘制在代表收益-风险的散点图中。
# 找到标准差最小数据的索引值
min_index = RandomPortfolios.Volatility.idxmin()
# 在收益-风险散点图中突出风险最小的点
RandomPortfolios.plot('Volatility', 'Returns', kind='scatter', alpha=0.3)
x = RandomPortfolios.loc[min_index,'Volatility']
y = RandomPortfolios.loc[min_index,'Returns']
plt.scatter(x, y, color='red')
plt.show()
绘制风险最小组合(GMV)的累积收益率曲线,并和等权重组合(EV)、市值加权的组合(MCap)进行比较。图中绿色曲线代表风险最小组合,它的收益率低于另外两种组合,这也符合人们对于风险小的投资收益相对较低的认知。
# 提取最小波动组合对应的权重, 并转换成Numpy数组
GMV_weights = np.array(RandomPortfolios.iloc[min_index, 0:numstocks])
# 计算GMV投资组合收益
StockReturns['Portfolio_GMV'] = stock_return.mul(GMV_weights, axis=1).sum(axis=1)
# 绘制累积收益曲线
cumulative_returns_plot(['Portfolio_EW', 'Portfolio_MCap', 'Portfolio_GMV'])
4.3 夏普最优组合
其实我们更想在收益和风险之间找到平衡点,夏普比率这个变量能帮我做出更好的决策,它计算的是每承受一单位的风险所产生的超额回报。更多关于夏普比率的计算可参考《用夏普比率分析股票的风险和回报》一文。
我们首先来计算上述蒙特卡洛模拟的组合所对应的夏普比率,并将之作为第三个变量绘制在收益-风险的散点图中,这里采用颜色这一视觉线索来表征夏普比率。
# 设置无风险回报率为0
risk_free = 0
# 计算每项资产的夏普比率
RandomPortfolios['Sharpe'] = (RandomPortfolios.Returns - risk_free) \
/ RandomPortfolios.Volatility
# 绘制收益-标准差的散点图,并用颜色描绘夏普比率
plt.scatter(RandomPortfolios.Volatility, RandomPortfolios.Returns,
c=RandomPortfolios.Sharpe)
plt.colorbar(label='Sharpe Ratio')
plt.show()
我们发现散点图上沿的组合具有较高的夏普比率。接着再找到夏普比率最大的组合,将其绘制在收益-风险的散点图中。
# 找到夏普比率最大数据对应的索引值
max_index = RandomPortfolios.Sharpe.idxmax()
# 在收益-风险散点图中突出夏普比率最大的点
RandomPortfolios.plot('Volatility', 'Returns', kind='scatter', alpha=0.3)
x = RandomPortfolios.loc[max_index,'Volatility']
y = RandomPortfolios.loc[max_index,'Returns']
plt.scatter(x, y, color='red')
plt.show()
最后绘制夏普最优组合(MSR)的累积收益曲线(下图中的红色曲线),发现它的收益远远高于其他组合。当然,我们用的是历史数据,至于能否在未来获得同样好的表现,还有待考量。
# 提取最大夏普比率组合对应的权重,并转化为numpy数组
MSR_weights = np.array(RandomPortfolios.iloc[max_index, 0:numstocks])
# 计算MSR组合的收益
StockReturns['Portfolio_MSR'] = stock_return.mul(MSR_weights, axis=1).sum(axis=1)
# 绘制累积收益曲线
cumulative_returns_plot(['Portfolio_EW', 'Portfolio_MCap', \
'Portfolio_GMV', 'Portfolio_MSR'])
注:本文是 DataCamp 课程 Intro to Portfolio Risk Management in Python 的学习笔记。