『运筹OR帷幄』原创

昨天实习摸鱼的时候开始看《Quantitative Equity Portfolio Management:Modern Techniques and Applications》,第一章讲到了经典的几个组合优化模型,于是乎干脆顺便在聚宽的策略环境里实现了一下,并选取了一个多因子选股进行了对比测试,初步证明了经典组合优化的一定有效性。个人觉得在实际交易场景中使用优化模型的主要难点在于如何较好的估计不同资产间的协方差矩阵以及各自的期望收益率。实证分析显示带有依赖资产预期收益率约束的优化问题,强行使用过去某段时间区间内的收益率替代预期收益率会产生严重的估计偏误从而严重影响优化结果。我觉得在实际使用这类模型以下两个问题需要被考虑:

1 如何有效的估计约束依赖的指标 ,如资产的预期收益率向量,资产的 \beta 向量,资产的协方差矩阵等等。关于这一块应该也已经有不少的工作,我了解比较少,但所知道的比如BL模型通过将投资者观点和先验收益率结合产生后验收益率输入均值方差优化模型,或者通过Compression Matrix的方法优化协方差矩阵都能较好的改进通过历史波动率来估计协方差矩阵而带来的种种弊端:比如,随着资产数目二次增加的估计时间复杂度,优化结果对权重的初始化高度敏感等等,这里不再展开。

2 如何考虑资产之间的序列相关性 ,通过简单的实证分析,对于大类资产间的配比如权益国债黄金,资产之间的相关性较低,我们更希望协方差是一个对角矩阵 D=\text{diag}\left\{ \sigma_{1}^{2},\sigma _{2}^{2},...,\sigma _{n}^{2} \right\} ,通过一些诸如masking或者shrinkage的trick可以较好的实现,而如果是纯股票组合,有时候考虑序列相关性的优化结果更好(我未做深入研究)。回到正题,我大致实现了以下几种常见优化。首先设置我们的原始股票列表,以及定义一个函数返回该组资产过去 天的日收盘价序列。

import numpy as np   
import pandas as pd 
import scipy.optimize as sco
universe = get_index_stocks('000300.XSHG')
stock_list = universe[0:5]#为了方便演示这里只选取五只股票构建投资组合
'''给定股票列表得到过去245个交易日的收盘价面板'''
def get_closedata(stock_list):
    close_dict = {}
    for i in range(len(stock_list)):
        code_name = stock_list[i]
        close_seq = attribute_history(stock_list[i],245,'1d', ('close'))
        close_seq = list(close_seq['close'])
        close_dict[code_name] = close_seq
        #transofrm to dataframe
    df_stock = pd.DataFrame(close_dict,columns = list(close_dict.keys()))
    return df_stock #255*N的DataFrame,每列对应一只股票过去245个交易日股价

1 以最大化期望收益率建立优化问题

优化问题:

'''全局最大化期望收益率组合'''
def cal_expret(weights:list,df_stock):
    risk_free_rate = 0.03
    if len(weights) != df_stock.shape[1]:
        raise Exception("the weights should be same size as stock list")
    weights = np.array(weights/np.sum(weights))
    returns_daily = np.log(df_stock/df_stock.shift(1)).iloc[1:,:]
    expected_return = np.sum(returns_daily.mean()*weights)*244-risk_free_rate
    return expected_return
def max_expret(weight0:list,df_stock):
    stock_num = df_stock.shape[1]
    w_min = 1/(np.power(stock_num,1.5))
    w_max = 0.6
    df = df_stock
    def maxexpret(weights):
        return cal_expret(weights,df)*(-1) #minimize 
    cons = ({'type':'eq', 'fun':lambda x: np.sum(x)-1})
    bnds = tuple((w_min,w_max) for x in range(stock_num)) 
    optimize = sco.minimize(maxexpret, weight0 , method = 'SLSQP', bounds = bnds,constraints = cons)
    optim_weight = list(optimize['x'].round(4))
    return optim_weight

注意我的代码里未使用让权重[0,1]的约束,而是让权重约束在:

其中 N 为组合的资产数目,在本例中 N=5

2 全局最小方差

优化问题:

'''全局最小方差组合'''
def cal_variance(weights:list,df_stock):
    if len(weights) != df_stock.shape[1]:
        raise Exception("the weights should be same size as stock list")
    weights = np.array(weights/np.sum(weights)) #normalizing
    #derive the portfolio volatility 
    returns_daily = (np.log(df_stock/df_stock.shift(1))).iloc[1:,:]
    Sigma = returns_daily.cov()*244
    Portfolio_vol = np.sqrt(np.dot(weights.T,np.dot(Sigma,weights)))
    return Portfolio_vol
def min_variance(weight0:list,df_stock):
    stock_num = df_stock.shape[1]
    w_min = 1/(np.power(stock_num,1.5))
    w_max = 0.6
    df = df_stock
    def minvariance(weights):
        return cal_variance(weights,df) #minimize 
    cons = ({'type':'eq', 'fun':lambda x: np.sum(x)-1}) #weights的求和=1
    bnds = tuple((w_min,w_max) for x in range(stock_num)) #w_i的上下限约束
    optimize = sco.minimize(minvariance,weight0,method = 'SLSQP',bounds = bnds, constraints = cons)
    optim_weight  = optimize['x'].round(4) 
    return optim_weight

3 最大化夏普率

优化问题:

'''最大化夏普率组合'''
def cal_sharpe(weights:list,df_stock):
    risk_free_rate = 0.03
    if len(weights) != df_stock.shape[1]:
        raise Exception("the weights should be same size as stock list")
    weights = np.array(weights)/np.sum(weights)
    returns_daily = np.log(df_stock/df_stock.shift(1)).iloc[1:,:]
    expected_return = np.sum(returns_daily.mean()*weights)*244-risk_free_rate #annualized excess return
    Sigma = returns_daily.cov()*244
    Portfolio_vol = np.sqrt(np.dot(weights.T,np.dot(Sigma,weights)))
    Sharpe_ratio = expected_return/Portfolio_vol
    return Sharpe_ratio
def max_sharpe(weight0:list,df_stock):
    stock_num = df_stock.shape[1]
    w_min = 1/(np.power(stock_num,1.5))
    w_max = 0.5
    df = df_stock
    def maxsharpe(weights):
        return cal_sharpe(weights,df)*(-1)
    cons = ({'type':'eq', 'fun':lambda x: np.sum(x)-1}) #weights的求和=1
    bnds = tuple((w_min,w_max) for x in range(stock_num)) #w_i的上下限约束
    optimize = sco.minimize(maxsharpe,weight0,method = 'SLSQP',bounds = bnds, constraints = cons)
    optim_weight  = optimize['x'].round(4) 
    return optim_weight

4 最大化效用函数(Utility Function)

效用函数期望尽可能的在最大化期望收益率和最小组合波动率之间取得一个平衡,带有一个penalty

'''效用函数最优化: 同时考虑期望收益率和组合风险'''
def cal_Utility(weights:list,df_stock,lambda_): #lambda 表示风险厌恶系数
    if len(weights) != df_stock.shape[1]:
        raise Exception("the weights should be same size as stock list")
    num = len(weights)
    weights = np.array(weights/np.sum(weights)) 
    #first part
    returns_daily = np.log(df_stock/df_stock.shift(1)).iloc[1:,:]
    first_part = np.sum(returns_daily.mean()*weights)*244
    #second part
    Sigma = returns_daily.cov()*244
    Portfolio_vol = np.sqrt(np.dot(weights.T,np.dot(Sigma,weights)))
    second_part = 0.5*lambda_*Portfolio_vol
    return first_part-second_part #w.T*return - 0.5*lambda*sigma
def max_utility(weight0:list,df_stock,lambda_):
    stock_num = df_stock.shape[1]
    w_min = 1/(np.power(stock_num,1.5))
    w_max = 1
    df = df_stock
    def maxutility(weights):
        return cal_Utility(weights,df,lambda_)*(-1)
    cons = ({'type':'eq', 'fun':lambda x: np.sum(x)-1}) #weights的求和=1
    bnds = tuple((w_min,w_max) for x in range(stock_num)) #w_i的上下限约束
    optimize = sco.minimize(maxutility,weight0,method = 'SLSQP',bounds = bnds, constraints = cons)
    optim_weight  = optimize['x'].round(4) 
    return optim_weight

5 风险平价

考虑股票的收益率序列相关性,使每只股票的资产的风险贡献度相同。有不考虑序列相关性的版本读者可自己完成。使用向量求导的法则将组合风险对W求导得到边际贡献度向量,再和权重向量做point-wise product即可得到风险贡献向量:

我们希望每一个元素尽可能相等,我借鉴了MSE损失函数的构造逻辑自己改进了损失函数:

'''风险平价:考虑不同个股之间的序列相关性'''
def RiskParityLoss(weights:list,df_stock):
    if len(weights) != df_stock.shape[1]:
        raise Exception("the weights should be same size as stock list")
    num = len(weights)
    weights = np.array(weights/np.sum(weights)) #normalizing
    #derive covariance matrix
    returns_daily = (np.log(df_stock/df_stock.shift(1))).iloc[1:,:]
    Sigma = returns_daily.cov()*244
    Portfolio_vol = np.sqrt(np.dot(weights.T,np.dot(Sigma,weights))) #组合波动率
    MRC = np.dot(Sigma,weights)/Portfolio_vol #边际风险贡献向量
    RC = MRC*weights #风险贡献
    RPLoss = 0
    for i in range(1,num):
        for j in range(i):
            RPLoss += (RC[i]-RC[j])**2
    return RPLoss
def Riskparity(weight0:list,df_stock):
    stock_num = df_stock.shape[1]
    df = df_stock
    def max_riskparity(weights):
        return RiskParityLoss(weights,df) #minimize 
    cons = ({'type':'eq', 'fun':lambda x: np.sum(x)-1}) #weights的求和=1
    bnds = tuple((0,1) for x in range(stock_num)) #风险平价只做(0,1)约束
    optimize = sco.minimize(max_riskparity, weight0 , method = 'SLSQP', bounds = bnds,constraints = cons)
    optim_weight = list(optimize['x'].round(4))
    return optim_weight

对于海外的多空对冲基金,一般还会在约束条件里加上\beta neutral 和 dollar neutral,感兴趣的读者可以基于以上代码自己实现。对于\beta 的估计可以简单的使用CAPM进行回归估计。我自己的一些想法: 如果在市场下行的时候,组合的Beta能尽可能接近0,则组合将可能不受市场趋势影响,因此一个可能的Idea是考虑优化:

其中第一项组合\betaL_2范数表示相对0的绝对偏移量,而第二项旨在最小化组合的波动率。

6 简单实证:以全局最小方差组合为例

这里我仅提供一个案例来说明这些经典优化的一定有效性。考虑构造一个rank-based多因子选股模型:经过单因子测试,我们得到一组有效因子f_1,...,f_k,我们进行因子合成得到单因子:

换仓周期为30天,不剔除停牌的股票,选取换仓日该因子排名最高的30只股票,进行全局最小方差优化,和无优化组合对比。回测周期2011到2021全年。

无优化组合:

全局最小方差组合:

在风险控制方面,原始组合的波动率为0.254,优化组合的波动率为0.21,下降了近20%;原始组合的最大回撤为48.3%,优化组合的最大回撤为38.5%,下降近20%,原始组合的超额收益率为217%,而优化组合的超额收益为321%,高出近50%。

进一步的分析显示,适当增加选股数目,全局最小方差组合在市场长期下行风格中抗跌性能良好,最小回撤较小。更多的结论可以由读者进行多种市场风格切换下不同组合优化模型的表现得到。欢迎同行私戳我进行idea交流(去读master前还能摸会鱼)。

在实际交易场景中使用优化模型的主要难点在于如何较好的估计不同资产间的协方差矩阵以及各自的期望收益率。实证分析显示带有依赖资产预期收益率约束的优化问题,强行使用过去某段时间区间内的收益率替代预期收益率会产生严重的估计偏误从而严重影响优化结果。...... 前文中,我们已介绍了许多量化投资思想,在这篇文章中,你将了解Markowitz投资组合优化的基本思想,以及如何在Python中实现。然后,我们将展示如何创建一个简单的策略回测,以Markowitz最佳方式重新平衡其投资组合,对各个资产的权重进行调整。 文章开始,我们将使用随机数据而不是实际的股... 给定几个已知的股市因素(开盘、收盘、最高、最低、成交量、成交额)及各因素对应的大量数据,训练一个该股票的涨跌趋势的预测模型。并在给定的测试数据的条件下求出接下来的涨跌趋势。即得到下图中的label值。-1代表跌、1代表涨。 1、LSTM简单介绍 LSTM这个算法是专门训练有时间序列信息的数据的,即这些数据不仅按照时间递增的顺序排布,并且前后的数据都有着很强的联系。个人认为与马尔可夫的思想差不多,即后面的值由前面的值来决定。本次需求是要根据已知的股市数据来分析某个时间段的涨跌趋势,并预测
题目描述在股市的交易日中,假设最多可进行两次买卖(即买和卖的次数均小于等于2),规则是必须一笔成交后进行另一笔(即买-卖-买-卖的顺序进行)。给出一天中的股票变化序列,计算一天可以获得的最大收益。遍历算法(复杂度较高)public class MaxProfit { public static void main(String[] args) { int samples[] = {10,22...
LM算法(Levenberg-Marquardt算法)是一种非线性最小二乘优化算法,可以用来求解非线性优化问题,例如优化双目相机的外参数。在本文中,我们将使用C++语言一步一步地实现LM算法优化算法来优化双目相机的外参数。 1. 确定优化目标函数 首先,我们需要确定双目相机外参数的优化目标函数。在本文中,我们将使用重投影误差作为优化目标函数。重投影误差是指将三维点投影到左右两个相机上得到的二维点与实际二维点之间的差异,它的计算公式如下: e = \sum_{i=1}^{N} \left\| u_i - \Pi_R(\mathbf{R}(\mathbf{X}_i - \mathbf{T})) - \mathbf{b} \right\|^2 其中,$u_i$是第$i$个三维点在左相机上的实际二维点,$\Pi_R$是右相机的投影矩阵,$\mathbf{R}$和$\mathbf{T}$是左相机到右相机的旋转矩阵和平移向量,$\mathbf{X}_i$是第$i$个三维点在世界坐标系下的坐标,$\mathbf{b}$是左相机和右相机之间的基线向量。 2. 实现LM算法 接下来,我们将使用LM算法来优化双目相机的外参数。具体步骤如下: (1)初始化参数 我们需要初始化双目相机的外参数,包括左相机到右相机的旋转矩阵和平移向量。在本文中,我们将使用随机数生成初始值。 (2)计算雅可比矩阵 我们需要计算优化目标函数的雅可比矩阵,它可以用来计算LM算法的增量方程。雅可比矩阵的计算公式如下: \mathbf{J}=\begin{bmatrix} \frac{\partial e_1}{\partial \mathbf{p}} & \frac{\partial e_2}{\partial \mathbf{p}} & \cdots & \frac{\partial e_N}{\partial \mathbf{p}} \end{bmatrix} 其中,$\mathbf{p}$是优化参数,包括左相机到右相机的旋转矩阵和平移向量。$\frac{\partial e_i}{\partial \mathbf{p}}$是第$i$个三维点对应的雅可比矩阵。 (3)计算增量方程 根据LM算法的公式,我们可以计算出增量方程: (\mathbf{J}^T\mathbf{J} + \lambda \mathbf{I})\Delta \mathbf{p} = \mathbf{J}^T\mathbf{e} 其中,$\lambda$是LM算法的阻尼因子,$\mathbf{I}$是单位矩阵,$\Delta \mathbf{p}$是参数的增量,$\mathbf{e}$是误差向量。 (4)更新参数 根据增量方程,我们可以计算出参数的增量,然后更新参数: \mathbf{p} = \mathbf{p} + \Delta \mathbf{p} (5)更新阻尼因子 如果新的误差比旧的误差更小,那么我们可以继续减小阻尼因子,以便更快地收敛。如果新的误差比旧的误差更大,那么我们应该增加阻尼因子,以避免步长过大导致的震荡。 (6)重复迭代 重复执行步骤2-5,直到误差收敛或达到最大迭代次数。 3. 代码实现 下面是LM算法优化算法的C++代码实现