用Python分析多股票的投资组合

俗话说不要将所有的鸡蛋放在同一个篮子里,在投资股票的时候我们也会多买几只以抵抗风险。本文将带领着你使用Python,来分析多只股票投资时的收益和风险,并找到最优的投资组合方案。这是上一篇文章《用Python分析股票的收益和风险》 的多股票升级版本。

本文目录如下:

一、股票数据的在线获取

二、投资组合的收益计算

三、相关性和协方差

四、寻找最优的投资组合

一、股票数据的在线获取

我们打算用以下9家大公司的股票构建投资组合,并用2017年的历史数据进行回溯测试。

公司名 股票代码
Apple AAPL
Microsoft MSFT
Exxon Mobil XOM
Johnson & Johnson JNJ
JP Morgan JPM
Amazon AMZN
General Electric GE
Facebook 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%
Facebook 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
Facebook 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 投资组合的标准差

投资组合的风险可以用标准差来衡量,只要知道组合权重和协方差矩阵,就可以通过以下公式进行计算。

\sigma = \sqrt{w_T \cdot \Sigma \cdot w}

  • \sigma:投资组合的标准差
  • \Sigma:收益的协方差矩阵
  • w:投资组合的权重(w_T是权重的转置)
  • \cdot 是点积运算

在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 的学习笔记。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容