# 以下都是例行公事,直接拷贝即可
import pandas as pd
import numpy as np
# 导入matplotlib.pyplot绘图库,其中plt.plot()是最常用的绘图函数之一
import matplotlib.pyplot as plt
import seaborn as sns
# 默认用seaborn的绘图样式
sns.set_theme()
"font.sans-serif"]=["Microsoft YaHei"] #设置字体。如果不设置,中文会乱码。这里采用微软雅黑'Microsoft YaHei',如果显示不正常,也可以使用黑体'SimHei'或者宋体'SimSun'等
plt.rcParams["axes.unicode_minus"]=False #该语句解决图像中的“-”负号的乱码问题
plt.rcParams[
# 绘图使用'svg'后端:svg是矢量格式,可以任意缩放均保持清晰,各种屏幕的显示良好。
%config InlineBackend.figure_formats = ['svg']
27 投资组合理论
27.1 快速复习
这里快速复习一下Markowitz资产组合模型,详情可见大部分投资学教材(如博迪的《投资学》)的相关章节。
27.1.1 2种资产的组合
考虑2种资产的组合,期望收益分别是\(r_1,r_2\),各自的方差分别是\(\sigma_1^2, \sigma_2^2\)。投资的权重是\(w_1, w_2\),其中\(w_1+ w_2 = 1\)。
那么组合期望收益就是:(简便起见这里就不写成期望形式了)
\[ r_p = w_1 r_1 + w_2 r_2 \]
组合的方差就是:
\[ \sigma_p^2 = w_1^2 \sigma_1^2 + w_2^2 \sigma_2^2 + 2 w_1 w _2 \operatorname{Cov}(r_1,r_2) \]
一个随机变量的方差,就是它自己和自己的协方差,因此上式又可以写成:
\[ \sigma_p^2 = w_1^2 \operatorname{Cov}(r_1,r_1)+ w_2^2 \operatorname{Cov}(r_2,r_2) + 2 w_1 w _2 \operatorname{Cov}(r_1,r_2) \]
上面的式子可以用协方差矩阵的形式来表示:
协方差矩阵 | \(w_1\) | \(w_2\) |
---|---|---|
\(w_1\) | \(\operatorname{Cov}(r_1,r_1)\) | \(\operatorname{Cov}(r_1,r_2)\) |
\(w_2\) | \(\operatorname{Cov}(r_2,r_1)\) | \(\operatorname{Cov}(r_2,r_2)\) |
可见,组合的方差,就等于上面的矩阵中的每一个协方差,乘以对应的权重,再相加。
直接从上式可以得到结论:
- 如果2种资产是负相关,那么他们的协方差就是负数(看最后一项),组合的方差必然下降。
- 如果2种资产是正相关,但相关系数小于1,组合的标准差依然会低于2项资产的标准差的加权平均。
注意到,收益和方差计算中,唯一的变量是权重,那么我们可以把每一个权重,对应的收益和标准差计算出来,绘制在一张图上。
- 选择第1个权重,如(0,1),计算代入2个公式,计算\(r_{p1},\sigma_{p1}\)
- 选择第2个权重,如(0.1,0.9),计算代入2个公式,计算\(r_{p2},\sigma_{p2}\)
- 选择第3个权重,如(0.2,0.8),计算代入2个公式,计算\(r_{p3},\sigma_{p3}\)
- …
把上述收益和标准差的组合绘制出来,你会得到下图中一组曲线的其中一条,称为“投资组合可行集”。
横轴是标准差,纵轴是收益。(图来自博迪《投资学》11版)
如果2种资产的相关性:
- 等于1,则组合的标准差是2者标准差的线性组合。
- 小于1,则组合会有一个最小方差点(最小标准差点):在这个组合下你可以获得最小方差。(两天蓝色线)
- 等于-1,则某个组合可以让最小方差为0。
27.1.2 资产配置
引入无风险资产,比如国债等等。考虑无风险资产和风险资产的组合。因为无风险资产和组合之间的协方差为0,那么无风险资产和风险资产的组合的期望收益和方差的关系,就是一条直线。
称为:“资本配置线(CAL)”:
- 如果全部投资于无风险资产,那么你获得了无风险收益率,同时组合方差为0:射线的最左端。
- 随着你增加风险资产的比例,那么方差也等比增加:因此是一条射线。
- 风险资产组合有无限种(权重的不同),反映为蓝色曲线(投资组合可行集)上的每一个点。那么纳入无风险资产,就是从\(r_f\)点,引出一条射线,穿过蓝线上的某个点。
- 射线斜率就是夏普比率:衡量每单位风险,能够获得多少超额收益。
- 所以穿过哪个点是最优的?斜率最高那条,即切线,则切点为“最优风险组合”。
\[ S_p=\frac{\mathrm{E}\left(r_p\right)-r_f}{\sigma_p} \]
27.1.3 马科维茨资产组合选择模型
把2种资产的情况扩展到n种,那么上面的2资产公式可以推为:
组合的期望收益:
\[ r_p = \sum_{i=1}^n {w_i r_i} \]
组合的方差:
\[ \sigma_p^2 = \sum_{j=1}^n \sum_{i=1}^n w_i w_j \operatorname{Cov}(r_i,r_j) \]
可以想象把前面的矩阵扩展到n行n列。
令\(w\)为权重,\(\Sigma\)为协方差矩阵,上面的式子可以写成矩阵:
\[ \sigma_p^2 = w^t \Sigma w \]
(w的转置,乘以协方差矩阵,乘以w)
我们要做的,就是用蒙特卡洛模拟的方法(近似可以理解为穷举法),尝试极大量的组合,计算他们的收益和标准差,绘制在图上,最后计算出最佳的组合权重。
蛮力计算正是电脑擅长的。
27.2 组合的预期收益和方差
使用数据returns.xlsx
,里面包含了3只股票的日回报率数据。
注:为什么要用日回报率数据?因为日回报率不用考虑分红、拆分或者复权之类的因素。当然,你用复权价格数据反算也是一样的。
# 读取数据并查看
= pd.read_excel('data/returns.xlsx',converters={'日期':pd.to_datetime})
df
'日期',inplace=True)
df.set_index( df.head()
比亚迪 | 美的集团 | 平安银行 | |
---|---|---|---|
日期 | |||
2019-01-02 | -0.035882 | -0.011394 | -0.020256 |
2019-01-03 | -0.022575 | -0.006586 | 0.009793 |
2019-01-04 | 0.053891 | 0.021823 | 0.050647 |
2019-01-07 | 0.014610 | 0.000000 | -0.001026 |
2019-01-08 | 0.028994 | -0.003514 | -0.008214 |
收益率等于 \[ r_1 = x_1/x_0 - 1 \]
对数收益率等于 \[ lr_1 = \ln {x_1/x_0} = \ln(r_1 + 1) \]
从对数收益率算收益率 \[ r_1 = e^{lr_1} - 1 \]
为什么要用对数收益率?因为对数收益率具有可加性:第1期期初,一直持有到第2期期末,持有2期的对数收益率,正好是2期各自对数收益率的直接相加。
\[ lr_{12} = lr_1 + lr_2 \]
- 要算一段时间的收益率,只要从第一个交易的对数收益率,一直累加,加到最后一个交易日的对数收益率,就是这段时间的对数收益率。
- 要算年化收益率,求整个序列的日平对数收益率,再乘以一年的交易日250即可。
从收益率计算对数收益率
= np.log(df + 1)
rets rets.head()
比亚迪 | 美的集团 | 平安银行 | |
---|---|---|---|
日期 | |||
2019-01-02 | -0.036542 | -0.011459 | -0.020464 |
2019-01-03 | -0.022834 | -0.006608 | 0.009745 |
2019-01-04 | 0.052489 | 0.021588 | 0.049406 |
2019-01-07 | 0.014504 | 0.000000 | -0.001027 |
2019-01-08 | 0.028582 | -0.003520 | -0.008248 |
投资1元的曲线
+1).cumprod().plot(); (df
*250 # 对数收益率的可加性,求“均值 * 一年的交易日数”,就是年化(对数)收益率 rets.mean()
比亚迪 0.688826
美的集团 0.531378
平安银行 0.385595
dtype: float64
* 250 # 年化协方差矩阵 rets.cov()
比亚迪 | 美的集团 | 平安银行 | |
---|---|---|---|
比亚迪 | 0.226201 | 0.054389 | 0.046846 |
美的集团 | 0.054389 | 0.098938 | 0.051926 |
平安银行 | 0.046846 | 0.051926 | 0.108442 |
27.2.1 给定权重的情况
首先,随机生成3个权重
# 证券数量 number of assets
= rets.shape[1]
noa noa
3
= np.random.random(noa) # 生成3个随机数
weights /= np.sum(weights) # 使3个随机数的和为1
weights weights
array([0.56598201, 0.22864297, 0.20537502])
sum() weights.
1.0
计算组合的方差
\[ \sigma_p^2 = w^t \Sigma w \]
* 250 , weights)) # 矩阵乘法满足结合律 np.dot(weights.T, np.dot(rets.cov()
0.11205046659344259
显然标准差就是方差开根号
* 250, weights))) np.sqrt(np.dot(weights.T, np.dot(rets.cov()
0.33473940101733257
写2个函数,计算在特定的权重下,股票的收益率和方差。
rets:对数回报率的序列 rets.mean():日均对数收益率 rets.mean() * weights :乘以权重后的日均对数收益率
* weights rets.mean()
比亚迪 0.001559
美的集团 0.000486
平安银行 0.000317
dtype: float64
def port_ret(weights):
"""年化对数收益率
"""
return np.sum(rets.mean() * weights) * 250
def port_vol(weights):
"""年化收益率的标准差
"""
return np.sqrt(np.dot(weights.T, np.dot(rets.cov() * 250 , weights)))
循环N次,每次随机生成一组权重,然后计算年化收益率和年化标准差。我们就可以获得N组(收益率, 标准差,权重)的组合。
= [] # 收益率
prets = [] # 标准差
pvols = []
w for p in range (5000): # 重复5000次
= np.random.random(noa)
weights /= np.sum(weights) # 每次重复,都随机取得3个权重。
weights
w.append(weights)# 计算组合收益
prets.append(port_ret(weights)) # 计算组合标准差
pvols.append(port_vol(weights))
= np.array(prets)
prets = np.array(pvols) pvols
27.2.2 最小方差点
= (np.where(pvols == min(pvols)))[0][0]
i round(3) # 权重 w[i].
array([0.131, 0.469, 0.4 ])
绘图
=(8, 5))
plt.figure(figsize=prets / pvols,
plt.scatter(pvols, prets, c='o', cmap='coolwarm',s = 5,alpha=0.5)
marker='Sharpe ratio(rf=0)'); #
plt.colorbar(label='k',s = 50,marker='x') # 最小方差点
plt.scatter(pvols[i],prets[i],c'SD')
plt.xlabel('预期收益率') ; plt.ylabel(
27.3 风险资产的有效边界
前面我们绘制了预期收益和标准差之间从关系,显然给定收益,正常人都会选择风险最小的组合,即风险资产的最小方差边界,就是上图中全部点所在区域的左侧边界。
在最小方差边界中,最小方差点之上的部分,称为风险资产有效边界。在这条线上点,表示给定收益的最小风险,或者给定风险的最大收益。
显然,我们实际会选择的组合必然在条线上。
27.3.1 最小标准差组合
我们的做法是给定一个收益率,比如0.55,然后求标准差最低的组合。
import scipy.optimize as sco
= 0.55 # 目标收益率
tret
= tuple((0, 1) for x in weights) # 3个权重的区间都是0到1
bnds
= ({'type': 'eq', 'fun': lambda x: port_ret(x) - tret},
cons 'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
{
= np.array(noa * [1. / noa, ])
eweights
= sco.minimize(port_vol, eweights, method='SLSQP',
res =bnds, constraints=cons) bounds
给定tret = 0.55,最小的标准差是
res.fun
0.28551471742741286
对应的权重是
res.x
array([0.27144962, 0.56311774, 0.16543264])
def find_min_sd(tret):
'''对于指定的收益率tret,找到最小标准差的组合,并返回标准差'''
= tuple((0, 1) for x in weights) # 3个权重的区间都是0到1
bnds
= ({'type': 'eq', 'fun': lambda x: port_ret(x) - tret},
cons 'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
{
= np.array(noa * [1. / noa, ])
eweights
= sco.minimize(port_vol, eweights, method='SLSQP',
res =bnds, constraints=cons)
bounds
return res.fun
0.55) find_min_sd(
0.28551471742741286
27.3.2 计算有效边界
对于0.45到0.68之间的收益率,切成50段,对其中每一个收益率,都计算小标准差组合。
= np.linspace(0.45, 0.68, 50)
trets
= np.array([find_min_sd(tret) for tret in trets]) tvols
= np.where(tvols == min(tvols))[0][0] i
=(8, 5))
plt.figure(figsize=prets / pvols,
plt.scatter(pvols, prets, c='o', cmap='coolwarm',s = 5,alpha=0.5)
marker='Sharpe ratio(rf=0)'); #
plt.colorbar(label='blue',lw=3)
plt.plot(tvols[i:],trets[i:],c#plt.scatter(tvols[i], trets[i], c='k',s = 50,marker='x')
'SD')
plt.xlabel('预期收益率') ; plt.ylabel(
27.4 资本市场线
把无风险资产加入组合
令资本市场线 为 $ y = a + b x\(,有效边界为\)y=f(x)$,有几个条件:
- 截距=无风险利率
- 资本市场线和有效边界在\(x\)处相交
- 资本市场线和有效边界在\(x\)处斜率相等
令有有效边界为\(f(x)\),资本市场线会与有效边界相切,那么在切点\(x\)上,两者的一阶倒数相等。
\[ b = f'(x) \]
问题在于,有效边界是我们用模拟方法(大量的取点)计算出来的,有效边界的数据本质上以一系列的点,如何获得有效边界的一阶导数?
splrep()
函数:可以对给定的x序列和y序列,拟合一条样条曲线。splev()
函数:利用上一步获得的曲线,可以对x求函数值或者一阶导函数的值。
import scipy.interpolate as sci
= np.argmin(tvols)
ind = tvols[ind:] # 有效边界的横轴(标准差)
evols = trets[ind:] # 有效边界的纵轴(收益率)
erets
# 获得有效边界的样条曲线
= sci.splrep(evols, erets)
tck
def f(x):
''' 有效边界函数 (样条曲线逼近). '''
return sci.splev(x, tck, der=0)
def df(x):
''' 样条曲线函数的一阶导函数 '''
return sci.splev(x, tck, der=1)
对于任何一个\(x\),我们都可以获得有效边界的值\(f(x)\),以及一阶导数\(f'(x)\)
= 0.325
x print(f'{f(x)=}, {df(x)=}')
f(x)=array(0.60642485), df(x)=array(0.87363672)
我们令 \(p = \{a,b,x\}\),分别表示资本市场线的截距、斜率、以及切点的横坐标。
资本市场线的三个条件,可以写成方程组:
- 截距=无风险利率, \(a = r_f\) :
a = rf
- 资本市场线和有效边界在\(x\)处相交,\(f(x) = a + b*x\):
f(x) = a + b * x
- 资本市场线和有效边界在\(x\)处斜率相等,\(b = f'(x)\):
b = df(x)
用fsolve()
函数可以解这个方程,参数分别是:
- 方程组。
- 初始参数:这是你对解的合理的猜测。如果猜得太远,就无法给出正确的解。
我们猜测,无风险利率给定为0.05,切点大概在0.3附近,斜率猜测是1左右(45°),那么我们对p的猜测就是[0.05, 0.5, 0.3]
。
# 资本市场线的方程组
def equations(p, rf=0.05):
= p
a,b,x = rf - a
eq1 = a + b * x - f(x)
eq2 = b - df(x)
eq3 return eq1, eq2, eq3
# 求解方程组
= sco.fsolve(equations, [0.05, 0.5, 0.3]) opt
看看求解的结果:
# 分别是rf = a, b, x 。其中x是切点所在的SD。 opt
array([0.05 , 1.75627377, 0.29341009])
把求解的结果代入方程组,看看是不是所有方程都为0。
round(equations(opt), 4) np.
array([0., 0., 0.])
绘图
=(10, 6))
plt.figure(figsize
# 散点
=(prets - 0.05) / pvols,
plt.scatter(pvols, prets, c='.', cmap='coolwarm')
marker
# 有效边界
'b', lw=4.0)
plt.plot(evols, erets,
# 资本市场线
= np.linspace(0.0, 0.4)
cx 0] + opt[1] * cx, 'r', lw=1.5)
plt.plot(cx, opt[
# 切点
2], f(opt[2]), 'y*', markersize=15.0)
plt.plot(opt[
'SD')
plt.xlabel('预期收益率')
plt.ylabel(='Sharpe ratio')
plt.colorbar(label
0.25,0.5)) # 绘图的范围,可以自行调整
plt.xlim((0.35,0.7)); plt.ylim((
27.4.1 从切点反求组合
注意到,opt[2]
就是切点的横坐标(SD),那么f(opt[2])
就是切点的纵坐标(预期收益率)
按照我们前面求有效边界的方法:指定预期收益率,求最小化波动率的组合,代码基本一样。
= ({'type': 'eq', 'fun': lambda x: port_ret(x) - f(opt[2])}, # 指定的预期收益率
cons 'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
{
= sco.minimize(port_vol, eweights, method='SLSQP',
res =bnds, constraints=cons)
bounds
'x'].round(2) res[
array([0.31, 0.59, 0.1 ])
因此,切点组合就是:31%的比亚迪,59%的美的集团,10%的平安银行。进一步,你可以在无风险资产(\(r_f = 0.05\))和切点组合之间进行选择你希望的比例,构成最终组合。