Python金融分析(九):投资组合优化

引言

前面我们提到了许多金融模型需要用到正态分布的假设,MPT就是其中的一个,接下来就简要介绍一下MPT。

投资组合优化

我们接下来探讨马科维茨的现代资产组合理论,也就是MPT。MPT的核心就是均值-方差模型,基于均值来描述资产的收益,基于方差来描述资产的风险,从而得到了特定风险水平下投资者(风险厌恶)如何构建组合来最大化期望收益。

在特定风险水平下,使得期望收益最大的投资组合为有效组合,不同风险水平下的有效组合组成的边界称之为有效前沿。不同风险偏好的投资者会在不同的风险水平下选择其投资组合。

Markowitz_frontier

均值-方差分析

我们仍然以前述四种资产为例进行分析。假设不允许投资者在金融工具中设立空头头寸,只允许多头头寸,这意味着100%的投资者财富必须在可用工具之间进行分配,以使所有头寸均为正且头寸加起来达到100%。 那么,容易得到资产组合的期望收益率和方差为:

μp=E(Iwiri)=wTμσp2=E((rμ)2)=wTΣw\begin{aligned} \mu_p&=E(\sum_{I}w_i r_i) = w^T\mu\\ \sigma^2_p &= E((r-\mu)^2)= w^T\Sigma{w} \end{aligned}

其中, w=[w1,,w4]w=[w_1, \ldots, w_4] 为权重向量, Σ\Sigma 为四种资产收益率的协方差矩阵, Iwi=1,wi>0\sum_{I}w_i=1, w_i>0

蒙特卡洛模拟

我们通过蒙特卡洛模拟不同比例的投资组合,得到其波动性(标准差)和期望收益率,在整个平面出绘制出可行域,可行域的上边界就是有效前沿。

1
2
3
4
5
# Calculate the annualized mean returns and covariance matrix
rets = np.log(data / data.shift(1))
annual_mean_rets = rets.mean() * 252
# The covrance of random walk is in proportion to time
annual_cov_rets = rets.cov() * 252
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def port_ret(weights):
return np.sum(annual_mean_rets*weights)
def port_vol(weights):
return np.sqrt(np.dot(weights.T, np.dot(annual_cov_rets, weights)))
prets = []
pvols = []
noa = len(data.columns)
for p in range(100000):
weights = np.random.random(noa)
weights /= np.sum(weights)
prets.append(port_ret(weights))
pvols.append(port_vol(weights))
prets = np.array(prets)
pvols = np.array(pvols)
1
2
3
4
5
6
plt.figure(figsize=(15, 9))
plt.scatter(pvols, prets, c=prets / pvols,
marker='o', cmap='coolwarm')
plt.xlabel('expected volatility')
plt.ylabel('expected return')
plt.colorbar(label='Sharpe ratio');

output_35_0

从图上我们可以清晰地看到,在波动率相同的情况下,有不同的投资组合可以选择,但是我们最关心的就是上面最清晰的那条边界也就是有效前沿。

最优化投资组合

我们上面通过蒙特卡洛模拟能够将整个可行域可视化,那么如何得到“确切”的最优解呢?这就用到了我们前面提到的数值优化的方法,显然这是一个带约束的优化问题,如果我们将夏普比率最大作为优化目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import scipy.optimize as sco

# Function to be minimized
def min_func_sharpe_ratio(weights):
return -port_ret(weights) / port_vol(weights)

# Equality constraint
cons = ({'type':'eq', 'fun': lambda x: np.sum(x)-1})

# Bounds for the parameters
bnds = tuple((0, 1) for x in range(noa))

# Initial parameters
eweights = np.array(noa * [1. /noa,])
1
2
3
opts = sco.minimize(min_func_sharpe_ratio, eweights, method='SLSQP',
bounds=bnds, constraints=cons)
opts
     fun: -1.047072952146737
     jac: array([-0.00027794,  0.00021888,  0.00056295, -0.00085148])
 message: 'Optimization terminated successfully.'
    nfev: 31
     nit: 5
    njev: 5
  status: 0
 success: True
       x: array([0.46094944, 0.23611206, 0.23543833, 0.06750017])

而如果我们将最小化波动性最为优化目标,我们可以得到有效前沿的最左端:

1
2
3
optv = sco.minimize(port_vol, eweights, method='SLSQP',
bounds=bnds, constraints=cons)
optv
     fun: 0.10789487898601618
     jac: array([0.11474433, 0.11216656, 0.10798594, 0.1077927 ])
 message: 'Optimization terminated successfully.'
    nfev: 48
     nit: 8
    njev: 8
  status: 0
 success: True
       x: array([1.30104261e-18, 0.00000000e+00, 5.28790073e-01, 4.71209927e-01])

求解有效前沿

那么,只要对于不同的期望收益率,最小化其波动性(或者对于给定的波动性,最大化其收益率)就可以得到有效前沿上的点(严格来说,前者还可以求得无效边界)。

1
2
3
4
5
6
7
8
9
10
cons = ({'type': 'eq', 'fun': lambda x:  port_ret(x) - tret},
{'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bnds = tuple((0, 1) for x in weights)
trets = np.linspace(0.05, 0.25, 100)
tvols = []
for tret in trets:
res = sco.minimize(port_vol, eweights, method='SLSQP',
bounds=bnds, constraints=cons)
tvols.append(res['fun'])
tvols = np.array(tvols)
1
2
3
4
5
6
7
8
9
10
11
12
plt.figure(figsize=(15, 9))
plt.scatter(pvols, prets, c=prets / pvols,
marker='.', alpha=0.8, cmap='coolwarm')
plt.plot(tvols, trets, 'b', lw=4.0)
plt.plot(port_vol(opts['x']), port_ret(opts['x']),
'y*', markersize=15.0)
plt.plot(port_vol(optv['x']), port_ret(optv['x']),
'r*', markersize=15.0)
plt.xlabel('expected volatility')
plt.ylabel('expected return')
plt.colorbar(label='Sharpe ratio')
plt.show()

output_46_0

资本市场线

CAL与CML

投资者首先确定有效的风险资产组合(位于有效前沿上),然后将无风险资产添加到组合中。通过调整投资于无风险资产的投资者财富比例,可以实现无风险资产与有效风险资产组合中的任何风险回报情况。而这些组合会构成一条直线,这条直线就叫做资本配置线(capital allocation line, CAL),它的斜率等于选择的资产组合每增加一单位标准差上升的期望收益,截距为无风险利率。

对于整个市场而言,首先资本市场线(capital market line, CML)也是一条CAL。但是,其特殊性在于,在资本资产定价模型假设下,当市场达到均衡时,市场组合成为一个有效组合,而且这个均衡状态是唯一的,即CML与有效前沿相切,切点为均衡状态。容易发现,CML的斜率是所有具有可行解的CAL里面斜率最大的,意味着CML的夏普比率最大,也就是说,市场最优的投资组合就是夏普比率最大的投资组合。

求解均衡状态

如上所述,均衡状态是CML与有效前沿的切点,这就又变成了一个解方程问题。假设有效前沿和无风险利率已知,求一条过无风险利率点,且与有效前沿相切的直线。

切点处CML与有效前沿的函数值和一阶导数值相等。因此需要对有效前沿进行求导,但是我们并没有显式的有效前沿函数。为了解决这个问题,可以利用插值的方法解决:选择三次样条函数进行插值,能够满足一阶可导的需要。

1
2
3
4
5
6
7
8
9
10
11
12
13
import scipy.interpolate as sci
ind = np.argmin(tvols)
evols = tvols[ind:]
erets = trets[ind:]
tck = sci.splrep(evols, erets)

def f(x):
''' Efficient frontier function (splines approximation). '''
return sci.splev(x, tck, der=0)
def df(x):
''' First derivative of efficient frontier function. '''
return sci.splev(x, tck, der=1)

scipy.optimize模块中的fsolve()函数可以用来解方程组,我们根据切点的条件联立方程,假设无风险利率为0.01:

1
2
3
4
5
6
7
8
9
10
11
12
13
import scipy.optimize as sco

# p[0]--risk free rate
# p[1]--slope of CML
# p[2]--votality of tangency
def equations(p, rf=0.01):
eq1 = rf - p[0]
eq2 = rf + p[1] * p[2] - f(p[2])
eq3 = p[1] - df(p[2])
return eq1, eq2, eq3

opt = sco.fsolve(equations,[0.01, 0.5, 0.15])
opt
array([0.01      , 0.99400907, 0.19900814])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
plt.figure(figsize=(15, 9))
plt.scatter(pvols, prets, c=(prets - 0.01) / pvols,
marker='.', cmap='coolwarm')
plt.plot(evols, erets, 'b', lw=4.0)
cx = np.linspace(0.0, 0.3)
plt.plot(cx, opt[0] + opt[1] * cx, 'r', lw=1.5)
plt.plot(opt[2], f(opt[2]), 'y*', markersize=15.0)
plt.grid(True)
plt.axhline(0, color='k', ls='--', lw=2.0)
plt.axvline(0, color='k', ls='--', lw=2.0)
plt.xlabel('expected volatility')
plt.ylabel('expected return')
plt.colorbar(label='Sharpe ratio')
plt.show()

output_55_0

我们可以看一下在最优点的资产组合情况:

1
2
3
4
5
6
7
8
cons = ({'type': 'eq', 'fun': lambda x:  port_ret(x) - f(opt[2])},
{'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
res = sco.minimize(port_vol, eweights, method='SLSQP',
bounds=bnds, constraints=cons)
print(f'The optimal portfolo is: {res.x}')
print(f'The optiaml return is: {port_ret(res["x"])}')
print(f'The optimal voltality is: {port_vol(res["x"])}')
print(f'The optimal Sharpe ratio is: {port_ret(res["x"]) / port_vol(res["x"])}')
The optimal portfolo is:     [0.54371066 0.28277661 0.17027672 0.00323601]
The optiaml return is:       0.20781589358674787
The optimal voltality is:    0.19900820203661537
The optimal Sharpe ratio is: 1.044257932386686