本文基于马科维茨有效集理论,利用Python构建一个有效边界。假设有一投资组合,含有5支股票:中国平安(000001)、格力电器(000651)、爱尔眼科(300015)、002156(通富微电)、长安汽车(000625),如何构建投资组合?
本文利用AKShare包获取股价数据。AKShare包是一个免费、开源的 Python 财经数据接口包。AKShare返回的绝大部分的数据格式都是 pandas DataFrame 类型,非常便于用 pandas/ NumPy/ Matplotlib 进行数据分析和可视化。
import pandas as pd
import numpy as np
import akshare as ak
# 读入5支股票 2021-01-01 到 2021-12-31日收盘价数据
def get_close(code):
data = ak.stock_zh_a_hist(symbol=code, period='daily', start_date='20180101', end_date='20211231', adjust='')
data.index = pd.to_datetime(data['日期'],format='%Y-%m-%d') #设置日期索引
close = data['收盘'] #日收盘价
close.name = code
return close
codes=['000001','000651','300015','002156','000625']
df = pd.DataFrame()
for code in codes:
df_ = get_close(code)
df = pd.concat([df,df_],axis=1)
df = df.dropna()
为了更加直观展示这五只股票,本文绘制了股价历史走势折线图,代码如下:
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('whitegrid')
sns.set(font='SimHei') # 设置字体
plt.rcParams['axes.unicode_minus'] = False #正常显示负号
ax = df.plot(figsize=(10,6))
ax.set_xlabel('日期',fontsize=12)
ax.set_ylabel('收盘价',fontsize=12)
ax.set_title('股价历史走势',fontsize=18)
为了构建投资组合,我们首先需要将股价转化为股票收益率。本文计算对数收益率,代码如下:
import numpy as np
returns_day=np.log(df/df.shift(1)) #计算日度收益率
returns_day.dropna(inplace=True) #删除空值
#绘制日收益率波动图
ax_v = returns_day.plot(figsize=(10,6))
ax_v.set_xlabel('日期',fontsize=12) # x轴标注
ax_v.set_ylabel('日收益波动率',fontsize=12) # y轴标注
ax_v.set_title('股票日收益率波动',fontsize=18)
考虑到股票停盘和非交易日等因素,选取252天作为年化天数,这几只股票的年化收益率如下:
returns_annual = returns_day.mean()*252 #计算年化收益率
returns_annual
000001 0.048497
000651 -0.052044
300015 0.086419
002156 0.097144
000625 0.046377
dtype: float64
这几只股票的协方差矩阵如下:
cov_annual = returns_day.cov()*252 #计算协方差
cov_annual
000001 | 000651 | 300015 | 002156 | 000625 | |
---|---|---|---|---|---|
000001 | 0.123537 | 0.054046 | 0.044919 | 0.027672 | 0.044307 |
000651 | 0.054046 | 0.108922 | 0.053776 | 0.028833 | 0.044216 |
300015 | 0.044919 | 0.053776 | 0.298210 | 0.040677 | 0.053856 |
002156 | 0.027672 | 0.028833 | 0.040677 | 0.283799 | 0.070811 |
000625 | 0.044307 | 0.044216 | 0.053856 | 0.070811 | 0.286122 |
rf = 0.02
# 给定权重,求组合收益率、标准差、夏普比率
def statistics(weights):
weights = np.array(weights)
port_returns = np.dot(weights, returns_annual) #获取组合收益率
port_stdev = np.sqrt(np.dot(weights.T, np.dot(cov_annual, weights))) #获取组合标准差
port_sharpe = (port_returns - rf) / port_stdev #获取组合夏普比率
return np.array([port_returns, port_stdev, port_sharpe])
import scipy.optimize as sco
# 定义规划求解目标函数:标准差进行最小化
def port_vol(weights):
return statistics(weights)[1]
num_assets = len(cov_annual)
# 约束条件:权重之和为1
constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
# 目标函数输入参数(即权重)取值范围为0-1
bounds = tuple((0, 1) for x in range(num_assets))
min_vol_port = sco.minimize(port_vol, num_assets*[1./num_assets,],
method='SLSQP', bounds=bounds, constraints=constraints)
min_vol_port_weights = min_vol_port.x # 最小方差组合各资产权重
min_vol_port_statisitcs = statistics(min_vol_port_weights) # 最小方差组合的收益、标准差、夏普比率
min_vol_port_allocation = pd.DataFrame(min_vol_port_weights,index=cov_annual.columns,columns=['最小方差组合配置比例']) # 最小方差组合配置表格
min_vol_port_allocation = min_vol_port_allocation.applymap(lambda x:format(x,'.2%')) #设置数据格式为百分比两位数
print(min_vol_port_allocation)
最小方差组合配置比例
000001 30.82%
000651 37.38%
300015 8.56%
002156 14.48%
000625 8.76%
tag = ['收益率','标准差','夏普比率']
for i in range(len(tag)):
print(f'最小方差组合{tag[i]}:{min_vol_port_statisitcs[i]:.1%}')
最小方差组合收益率:2.1%
最小方差组合标准差:26.5%
最小方差组合夏普比率:0.4%
在Scipy的优化函数中,没有“最大化”的功能,因此作为一个目标函数,需要找到被最小化的变量值。因此我们定义neg_sharpe_ratio
函数来计算负的夏普比率。
# 定义规划求解目标函数:负的夏普比率进行最小化
def neg_sharpe_ratio(weights):
return -statistics(weights)[2]
num_assets = len(cov_annual)
# 约束条件:权重之和为1
constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
# 目标函数输入参数(即权重)取值范围为0-1
bounds = tuple((0, 1) for x in range(num_assets))
max_sharpe_port = sco.minimize(neg_sharpe_ratio, num_assets*[1./num_assets,],
method='SLSQP', bounds=bounds, constraints=constraints)
max_sharpe_port_weights = max_sharpe_port.x # 最大夏普比率组合各资产权重
max_sharpe_port_statisitcs = statistics(max_sharpe_port_weights) # 最大夏普比率组合的收益、标准差、夏普比率
max_sharpe_port_allocation = pd.DataFrame(max_sharpe_port_weights,index=cov_annual.columns,columns=['最大夏普比率组合配置比例']) # 最大夏普比率组合配置表格
max_sharpe_port_allocation = max_sharpe_port_allocation.applymap(lambda x:format(x,'.2%')) #设置数据格式为百分比两位数
print(max_sharpe_port_allocation)
最大夏普比率组合配置比例
000001 22.15%
000651 0.00%
300015 32.93%
002156 44.92%
000625 0.00%
tag = ['收益率','标准差','夏普比率']
for i in range(len(tag)):
print(f'最大夏普比率组合{tag[i]}:{max_sharpe_port_statisitcs[i]:.1%}')
最大夏普比率组合收益率:8.3%
最大夏普比率组合标准差:34.6%
最大夏普比率组合夏普比率:18.2%
目前我们有五只股票,那么在投资组合里我们应该如何对这五只股票进行资产配置呢?我们要对每只股票投资一定的权重,权重之和等于1 。接下来我们对投资组合里的每只股票生成随机权重,计算出投资组合每年的收益和波动性。
#随机生成1000个投资组合
port_returns_list = [] # 定义列表,存放组合年化收益率
port_stdev_list = [] # 定义列表,存放组合年化标准差
num_assets =len(cov_annual) # 组合中的资产数
for x in range(1000):
#设置不同的随机种子,生成和为1的权重
np.random.seed(x)
weights = np.random.random(num_assets)
weights /= np.sum(weights)
port_returns = np.sum(weights*returns_annual) # 组合年化期望收益率
port_stdev = np.sqrt(np.dot(weights.T ,np.dot(cov_annual ,weights))) # 组合年化标准差
port_returns_list.append(port_returns)
port_stdev_list.append(port_stdev)
port_returns = np.array(port_returns_list)
port_stdev = np.array(port_stdev_list)
port_returns_stdev = pd.DataFrame({'组合收益率':port_returns_list,'组合标准差':port_stdev_list})
port_returns_stdev = port_returns_stdev.applymap(lambda x:format(x,'.2%')) #设置数据格式为百分比两位数
port_returns_stdev.head()
组合收益率 | 组合标准差 | |
---|---|---|
0 | 4.02% | 27.97% |
1 | 1.19% | 26.98% |
2 | 6.91% | 31.27% |
3 | 3.59% | 29.10% |
4 | 5.24% | 28.88% |
已知投资组合的收益与风险,我们可以画出所有可行集。但为了得到有效边界,我们还需要求解给定收益率水平下的最小标准差。利用Scipy包中的scipy.optimize.minimize我们可以在一定约束条件下求解目标函数的最小值。
import scipy.optimize as sco
# 定义规划求解目标函数:标准差进行最小化
def port_vol(weights):
return statistics(weights)[1]
# 规划求解:给定收益,求最小标准差
target_returns = np.linspace(min_vol_port_statisitcs[0],0.09,500) # 定义不同目标收益率水平,最小为最小方差组合收益率
target_min_stdev = []
#初始值x0:
num_assets = len(cov_annual)
x0 = num_assets*[1./num_assets,]
for tar in target_returns:
#两个约束条件:(1)权重之和为1 (2)给定目标收益率
constraints = [{'type':'eq','fun':lambda x: sum(x)-1},{'type':'eq','fun': lambda x:statistics(x)[0]-tar}]
#目标函数输入参数(即权重)取值范围为0-1
bounds = tuple((0, 1) for x in range(num_assets))
outcome = sco.minimize(port_vol,x0 = x0,method = 'SLSQP',constraints = constraints,bounds = bounds) #求解
target_min_stdev.append(outcome.fun) #将最小标准差合并到列表中
target_min_stdev = np.array(target_min_stdev)
port_returns_min_stdev = pd.DataFrame({'组合收益率':target_returns,'组合最小标准差':target_min_stdev})
port_returns_min_stdev = port_returns_min_stdev.applymap(lambda x:format(x,'.2%')) #设置数据格式为百分比两位数
port_returns_min_stdev.head()
组合收益率 | 组合最小标准差 | |
---|---|---|
0 | 2.10% | 26.47% |
1 | 2.12% | 26.47% |
2 | 2.13% | 26.47% |
3 | 2.14% | 26.47% |
4 | 2.16% | 26.47% |
绘制有效边界的代码如下:
plt.scatter(port_stdev,port_returns,c=(port_returns-rf)/port_stdev,marker='o',cmap='YlGnBu') # 1000个随机组合
plt.plot(target_min_stdev,target_returns,label='有效前沿',linewidth=4) # 给定收益,方差最小的组合
plt.xlabel('组合标准差')
plt.ylabel('组合收益率')
plt.title('马科维茨有效边界')
plt.colorbar(label='夏普比率')
plt.legend()
plt.show()
plt.scatter(port_stdev,port_returns,c=(port_returns-rf)/port_stdev,marker='o',cmap='YlGnBu') # 1000个随机组合
plt.plot(target_min_stdev,target_returns,label='有效前沿',linewidth=4) # 给定收益,方差最小的组合
plt.xlabel('组合标准差')
plt.ylabel('组合收益率')
plt.title('马科维茨有效边界')
plt.plot()
plt.colorbar(label='夏普比率')
# 黄星:标记最小方差组合
plt.scatter(min_vol_port_statisitcs[1],min_vol_port_statisitcs[0],color='black',marker='*',s=150,label='最小方差组合')
# 红星: 标记最大夏普比率组合
plt.scatter(max_sharpe_port_statisitcs[1],max_sharpe_port_statisitcs[0],color='r',marker='*',s= 150,label='最大夏普比率组合')
plt.legend()
plt.show()
联系客服