精品教学案例 | 用Python构建有效投资组合

2020-05-19 17:39:36 浏览数 (1)

查看本案例完整的数据、代码和报告请登录数据酷客(http://cookdata.cn)案例板块。

本案例适合作为大数据专业数据清洗或数据可视化课程的配套教学案例。通过本案例,能够达到以下教学效果:

  • 培养学生处理真实股票数据的能力。案例中使用格力、美的、京东方、恒瑞医药、苏宁易购五家公司从2017年1月1日至2019年6月1日的日度股票收盘价数据,构建了一个有效投资组合。
  • 帮助学生熟悉数据获取、数据清洗、数据可视化等方法,以及金融相关的专业知识。例如:缺失值的检测和处理、如何绘制股票价格走势图、如何使用Python计算夏普比率等。
  • 提高学生动手实践能力。案例中使用Tushare包获取股票数据,使用Pandas、Seaborn、Matplotlib等工具对数据进行清洗和可视化操作,并最终使用Scipy包构建了一个有效投资组合。

金融科技(Financial Technology)正在越来越深刻地影响量化投资领域。量化投资领域最有名的公司当属量化交易之父——西蒙斯(James Simons)创立的文艺复兴对冲基金公司。文艺复兴对冲基金使用复杂的数学模型去分析并执行交易,通过寻找那些非随机行为来进行市场预测。从1989年起,文艺复兴对冲基金旗下的大奖章基金(Medallion)年回报率平均高达35%,被誉为最成功的对冲基金。当前,美国市场的量化对冲基金资产管理规模有望突破1万亿美元,约占总量的三分之一。而作为仅次于美国的全球第二大市场,中国股市量化投资的占比还很低,依然有很大的发展空间。量化投资的好处之一是可以避免人性的诸多弱点,并且可以从量化的角度分散化投资风险,因为它主要依赖于程序进行决策,减少了人的不理性决策。当然,在实际的应用过程中需要程序 人工的结合,忽视任何一方都可能会产生巨大的风险。

在量化投资领域,最流行的编程语言就是Python,主要原因是Python简单易上手,并且有诸多数据分析、可视化以及统计建模的包可供我们使用。Python已经越来越受到投资界的重视,高盛集团(Goldman Sachs)给出的一则报告称Python是未来金融从业人员的必备技能之一,高盛集团内部也已经裁掉一批金融交易员转而使用基于Python的程序化交易。本篇案例就旨在介绍如何使用Python进行量化投资,我们将从数据获取、数据清洗、数据可视化、构造有效投资组合几个方面介绍Python在量化交易中的应用。

1.使用Tushare包获取股票数据

股票交易数据的获取有诸多种方式,一些大型数据商如万得(Wind)会提供非常详细、实时更新的股票交易数据,我们也可以通过爬虫等方式获取股票交易数据。但是对于初学者而言,这些方法要么费时间,要么需要付费进行购买。这里,我们介绍一个国内免费、开源的财经数据接口包:Tushare,它可以方便快捷地帮助我们获取所需要的数据。我们使用Tushre.get_hist_data()函数来获取所需要的数据。

该函数的主要输入参数如下:

  • code:股票代码,即6位数字代码
  • start:开始日期,格式YYYY-MM-DD
  • end:结束日期,格式YYYY-MM-DD
  • type:数据类型,D=日k线;W=周;M=月;5=5分钟;15=15分钟;30=30分钟;60=60分钟。默认为D
  • retry_count:当网络异常后重试次数,默认为3

该函数的主要返回值如下:

  • date:日期
  • open:开盘价
  • high:最高价
  • close:收盘价
  • volume:成交量

我们选取的股票为格力美的京东方恒瑞医药苏宁易购五家股票,使用他们的日度收盘价格。我们选取的时间段为2017-01-012019-06-01,代码如下:

代码语言:javascript复制
import pandas as pd
import tushare as ts
import matplotlib.pyplot as plt
import seaborn as sns
import datetime
import numpy as np
import scipy.optimize as sco

#获取五只股票的数据,第一个为格力
geli=ts.get_hist_data('600848',start='2017-01-01',end='2019-06-01',ktype='D',retry_count=4)['close']
#更改列名,避免列名重复
geli.rename(columns={'close':'geli'}, inplace = True)

meidi=ts.get_hist_data('000333',start='2017-01-01',end='2019-06-01',ktype='D',retry_count=4)['close']
meidi.rename(columns={'close':'meidi'}, inplace = True)

jingdongfang=ts.get_hist_data('000725',start='2017-01-01',end='2019-06-01',ktype='D',retry_count=4)['close']
jingdongfang.rename(columns={'close':'jingdongfang'}, inplace = True)

hengruiyiyao=ts.get_hist_data('600276',start='2017-01-01',end='2019-06-01',ktype='D',retry_count=4)['close']
hengruiyiyao.rename(columns={'close':'hengruiyiyao'}, inplace = True)

suningyigou=ts.get_hist_data('002024',start='2017-01-01',end='2019-06-01',ktype='D',retry_count=4)['close']
suningyigou.rename(columns={'close':'suningyigou'}, inplace = True)

geli.head()

Tushre.get_hist_data()函数返回一个包含某只股票交易量、收盘价、开盘价等多种信息的DataFrame。因为这里我们需要的是收盘价,因此只需close一列即可,实际上我们得到的是5个时间序列。我们需要将它们合并到一个DataFrame里面,并且对该DataFrame按照时间顺序进行排序。

代码语言:javascript复制
#生成一个词典
stock_portfolio={
        '格力':geli,
        '美的':meidi,
        '京东方':jingdongfang,
        '恒瑞医药':hengruiyiyao,
        '苏宁易购':suningyigou
        }
#生成DataFrame
stock_portfolio=pd.DataFrame(stock_portfolio,index=jingdongfang.index)
#如果该package不稳定可以直接使用:stock_portfolio="./input/gupiao_portfolio.csv"
stock_portfolio.sort_index(axis=0,ascending=True,inplace=True) 

我们查看一下stock_portfolio的前几行,可知我们已经得到了五只股票的收盘价。

代码语言:javascript复制
stock_portfolio.head()

2.使用Pandas包进行股票数据清洗

获得股票数据之后,我们需要对其进行清洗,首先查看一下数据是否存在缺失值:

代码语言:javascript复制
stock_portfolio.info()

可以看到数据存在少量缺失值,因为股票有时候会被停牌。关于缺失值处理有多种方法,一种是:如果某一行有任何缺失值,则将其删除;另外一种常见的方法是:对缺失值进行填充,这里我们使用线性插值的方法填充缺失值,也就是我们使用缺失值的前一个(非缺失的)值和后一个(非缺失的)值的平均数来对缺失值进行填充。

代码语言:javascript复制
stock_portfolio.interpolate(method="linear",inplace=True)

再次查看是否存在缺失值,发现已经不存在缺失值。

代码语言:javascript复制
stock_portfolio.info()

3.使用Seaborn包进行股票数据可视化

股票数据最主要的问题就是有缺失值,除此之外不需要进行比较复杂的预处理,除非需要特定类型的数据,比如说把日度收益率转化为月度收益率。接下来我们进入数据可视化环节,首先看一下五家公司股票价格的历史走势如何:

代码语言:javascript复制
sns.set_style("darkgrid")
sns.set(font="SimHei")
sns.set_context("talk")
plt=stock_portfolio.plot(figsize=(12,10),title="股票价格历史走势",grid=False)
plt.set_xlabel('日期')
plt.set_ylabel('股票价格')

上图是五只股票的历史走势,但是我们看到虽然我们的索引是日期,但是这里并没有显示出日期,是因为我们没有将其变为日期型数据,下面我们将索引变为日期型,然后重新进行绘图:

代码语言:javascript复制
stock_portfolio.index=pd.DatetimeIndex(stock_portfolio.index)
plt2=stock_portfolio.plot(figsize=(12,10),title="股票价格历史走势",grid=False)
plt2.set_xlabel('日期')
plt2.set_ylabel('股票价格')

可以看到五家公司股票的走势的总体趋势类似,表明它们的股价都受到了同样的宏观因素的冲击,比如政府推行的减税政策对于全行业来说都是一个利好消息。但是股票肯定也会有一些公司层面的独特风险,比如公司的经营策略等,构建投资组合的目的就是为了降低公司层面的独特性风险。

在股票投资领域,我们其实关注的并不是股票的价格,而是股票收益率。如上图所示,恒瑞医药股价一直比较高,但是这并不意味着投资恒瑞医药就一定比投资京东方赚钱,接下来我们需要思考如何将股价换算为股票收益率,股票日度收益率的公式如下:

但是在实际应用中,这种方式需要的计算量比较大,因此我们一般使用:

计算日度收益率:

代码语言:javascript复制
stock_portfolio=np.log(stock_portfolio)-np.log(stock_portfolio.shift(1))
stock_portfolio.head()

显然,第一行是空值,因此我们将其去掉:

代码语言:javascript复制
stock_portfolio.dropna(inplace=True)
stock_portfolio.head()

接下来再次对收益率进行绘图,可以发现日收益率波动相对而言是非常小的,大多都在-5%到5%之间,格力恒瑞医药这两只股票相对而言波动比较大。

代码语言:javascript复制
plt_volatility=stock_portfolio.plot(figsize=(18,10),title="股票日收益率波动情况",grid=False)
plt_volatility.set_xlabel('日期')
plt_volatility.set_ylabel('日收益率波动范围')

理论上而言,在完全有效的金融市场,股票收益率应该服从期望收益为0的正态分布。接下来,我们查看一下五只股票的收益率分布:

代码语言:javascript复制
plt_dis=stock_portfolio.plot(kind="density",figsize=(10,8),title="股票日收益率分布状况",grid=False)
plt_dis.set_xlabel('日期')
plt_dis.set_ylabel('日收益率密度')
代码语言:javascript复制
plt_hist=stock_portfolio.hist(figsize=(20,5),grid=False,layout =(1,5))

从上面两幅图来看,除了恒瑞医药之外,其他四只股票的日收益率都接近正态分布。

接下来查看一下这五家股票的年化收益率:

代码语言:javascript复制
stock_portfolio.mean()*365 #当然,实际投资中必须考虑股票停牌以及周末等非交易日

可以看到,在我们考察的样本期内,格力的年化收益率最高,为36%左右,美的紧随其后,为33%。而苏宁易购最低,为-5.2%。当然,这是理想状况下的年化收益率,实际上必须考虑买入、卖出涉及的相关交易费用以及股票停牌等因素。不过结合市场实际状况来看,格力的营收能力确实非常强悍!

接下来看一下五只股票的波动性和相关性。股票波动性意味着风险,用方差来衡量,我们使用DataFrame.var()获取五只股票的方差。从五只股票的波动状况来看,格力的股票波动性最大,对应的年化方差为0.34,也就是说购买格力股票对应的风险也是最大的,这也印证了风险与收益是相对应的金融常识。

代码语言:javascript复制
stock_portfolio.var()*365

相关性在投资组合领域也非常重要,是分散化投资风险的核心思想。相关性越弱越有利于投资组合的分散化。相关性使用协方差或相关系数来衡量,我们使用DataFrame.cov()函数得到五只股票的协方差,可以看出五只股票的相关性都不是太强,并且五只股票并没有负相关——协方差都为正,一个原因可能是我们只选取了五只股票,而想要有效地分散投资风险,一般需要几十只股票。

代码语言:javascript复制
stock_portfolio.cov()*365

4.基于最优夏普比率构建股票投资组合

4.1如何计算投资组合的收益与方差

接下来进入到投资组合领域,我们需要寻找最优的投资组合。首先介绍一下什么是投资组合,然后再介绍何为最优。投资组合是指:将总资产按比例投入到不同的股票上,比如:这五只股票我们每一只都投入20%的总资产进行购买,也就是等权重投资。下面,我们以两种风险资产为例,介绍如何计算投资组合的期望收益和方差。

4.2夏普比率

那么什么是最优的投资组合呢?经典的金融学理论认为我们要寻找最优夏普比率(Sharp ratio)的投资组合,因为它衡量了承担一单位风险带来的收益补偿,夏普比率定义如下:

而根据我们上面使用五只股票进行的计算,我们已经得到了期望和协方差矩阵。再加上权重,我们便可以计算出投资组合的期望收益和协方差矩阵,进而计算出夏普比率,注意这里我们想计算的是持有期的有效投资组合,因此需要使用持有期投资收益率和持有期风险,因此我们将日度收益率和日度风险乘以投资持有的时间而不是365。代码如下:

代码语言:javascript复制
W=np.array([0.2]*5) #定义投资五只股票的权重,这里设为每只股票都是0.2
stock_mean=stock_portfolio.mean()*len(stock_portfolio.index)
stock_cov=stock_portfolio.cov()*len(stock_portfolio.index)
r_portfolio=np.dot(stock_mean,W) #得到投资组合的期望收益
var_portfolio=np.dot(W,stock_cov)
var_portfolio=np.dot(var_portfolio,W.T) #得到投资组合的方差
sharp_ratio=r_portfolio/var_portfolio  #得到夏普比率
print(sharp_ratio)

可以看到,夏普比率是1.84。

4.3寻找最优夏普比率的投资组合

我们已经知道了如何计算夏普比率,那么如何寻找最优的夏普比率呢?这实际上是一个有约束下的最优化问题,我们可以使用Scipy包中的最小优化算法函数sci.optimize.minimize来帮助我们进行求解,该算法可以在有约束的情况下最小化目标函数。因此,我们将上面的投资组合中各个股票的权重W设为自变量,因为这里是求最小化,我们定义负的Sharp Ratio作为目标函数。

代码语言:javascript复制
def min_sharp_ratio(W):
    W=np.array(W)
    stock_mean=stock_portfolio.mean()*len(stock_portfolio.index)
    stock_cov=stock_portfolio.cov()*len(stock_portfolio.index)
    r_portfolio=np.dot(stock_mean,W) #得到投资组合的期望收益
    var_portfolio=np.dot(W,stock_cov)
    var_portfolio=np.dot(var_portfolio,W.T) #得到投资组合的方差
    return -r_portfolio/var_portfolio  #返回负的夏普比率

约束条件为:

代码语言:javascript复制
cons = ({'type': 'eq', 'fun': lambda W: -np.sum(W)   1}) #投资组合权重之和为1

因为这里我们不允许买空和卖空股票,因此每一只股票的权重都必须限制在[0,1]之间。

代码语言:javascript复制
bnds = tuple((0, 1) for W in range(len(stock_portfolio.columns)))

给定一个初始权重:

代码语言:javascript复制
W0=np.array([0.2]*5)
代码语言:javascript复制
opts_sharpratio = sco.minimize(min_sharp_ratio, W0, method='SLSQP', bounds=bnds,constraints=cons)

可以得到五只股票的投资比例为:

代码语言:javascript复制
opts_sharpratio['x'].round(3)

此时的Sharp ratio是:

代码语言:javascript复制
-min_sharp_ratio(opts_sharpratio['x'].round(3))

如果我们想找到最小化投资组合方差的投资组合,也就是最小化投资组合风险而不管收益如何,需要重新定义目标函数:

代码语言:javascript复制
def min_var(W):
    W=np.array(W)
    stock_mean=stock_portfolio.mean()*len(stock_portfolio.index)
    stock_cov=stock_portfolio.cov()*len(stock_portfolio.index)
    r_portfolio=np.dot(stock_mean,W) #得到投资组合的期望收益
    var_portfolio=np.dot(W,stock_cov)
    var_portfolio=np.dot(var_portfolio,W.T) #得到投资组合的方差
    return var_portfolio  #返回投资组合方差
opts_var = sco.minimize(min_var, W0, method='SLSQP', bounds=bnds,constraints=cons)
print(opts_var['x'].round(3))

上面的权重即为最小化方差时各个股票所占的比重。

对应的夏普比率为:

代码语言:javascript复制
-min_sharp_ratio(opts_var['x'].round(3))

当然,有的时候我们想根据个人的效用函数求解最优的投资组合比例,这样也非常较简单,我们只需要重新定义效用函数;如果允许买空卖空,我们也只需要将边界条件放宽即可。

5.总结

本文介绍了如何使用Python构建有效投资组合,通过数据获取、数据清洗、数据可视化、构建投资组合、求解最优投资组合几个步骤,使读者可以对Python在金融科技、量化投资领域的应用有一个初步的认识。此外,本案例涉及的一些概念比如:收益、方差、夏普比率等均是投资学课程以及CFA考试的必考概念,通过学习本案例读者会对相关概念有更加直观深刻的了解。

0 人点赞