人生若只如初见,何事秋风悲画扇。等闲变却故人心,却道故人心易变。
--《木兰花》 纳兰容若
多因子模型的介绍文章汗牛充栋,但系统性的归纳整理首推石川博士的多因子系列文章,看完绝对让人有醍醐灌顶的感觉。其次大部分多因子文章都是聚焦于方法论层面的探讨,却很少有深入到代码实现层面的讲解,这对于大部分初次接触多因子模型的用户来说似乎总隔着一层窗户纸。秉着Talk is cheap,show me the code的理念,这里来和大家一起复现一下各券商金工研报中经常会提到的纯因子收益率的实现逻辑。由于是个人的理解,纰漏在所难免,欢迎指正。
那什么是纯因子收益率呢?它和我们通常谈论的多空组合因子收益率又有什么差异?
对于任意投资组合,如果该组合对于某个指定因子的暴露度为 1,而对其他因子的暴露度为 0,则称该投资组合为指定因子的纯因子组合。纯因子收益率的实现不同于我们常说的高低分组,因为高低分组求出的多空收益率并不能完全保证投资组合在其他因子上的暴露度恰好为0,比如高低估值分组求估值因子的收益率便会面临着其在市值因子上也存在着明显暴露,因为通常而言,高估值组对应小市值,低估值组对应大市值。
为了解决投资组合在其他因子上存在风格暴露的问题,一般是通过使用基于截面回归的加权最小二乘法(Weigthed Least Square, WLS)来计算投资组合不同因子下对应的股票权重。在复现之前,我们来简单回顾一下BARRA CNE5中的多因子模型框架。
考虑一个包含
只股票的投资组合,其可以用
个因子进行解释,
个因子中包括
个国家因子,
个行业因子,
个风格因子,即
。一般表达式如下:
为了便于理解,通常转换成矩阵的表现形式:
其中,各变量对应的含义如下:
个股超额收益率向量
因子暴露度矩阵
因子收益率向量
特异性收益率向量
可以看到,在该框架下,新引入了一个国家因子,用来代表全市场的收益率。但由于任意一支股票在所有行业的暴露度之和始终为1(行业暴露度是0、1哑变量),而单支股票在国家因子上的暴露度又始终为1,所以导致了国家因子和行业因子之间存在明显的共线性,进而导致矩阵不可逆,使得模型不存在唯一解。为此,我们需要对行业的因子收益率做一定的约束,通常的做法是使行业因子收益率按流通市值加权的和等于0,写成表达式如下:
其中,
表示行业
中所有股票的流通市值占全市场的流通市值的比例。考虑约束条件后,便可以将
个因子收益率转换成
个因子收益率的线性表达式,其中
。假设第
个行业因子被扣除,则可以表述为以下矩阵形式:
其中,中间的矩阵即为约束矩阵
在实际处理过程中,研究者发现不同股票的特质收益率是存在异方差现象的,且通常方差大小与股票的市值的平方根成反比关系。我们知道,当存在异方差现象时,普通最小二乘法(OLS)回归得到的结果虽然是线性无偏的,但不是有效的,此时显著性检验失去了作用,预测效果变得糟糕。基于此,通常采用加权最小二乘法(WLS)来代替普通最小二乘法(OLS)。为了实现异方差到同方差的转换,权重调整矩阵设定为对角阵,而对角阵对角线上的数值为每一支股票的流通市值的平方根占全部股票流通市值平方根之和的比率,这里用变量
表示,具体而言,
准备工作就绪,我们来看看不同条件下线性回归结果的表现形式在无约束条件下,不考虑异方差时,通过OLS计算得到的投资组合权重为:
在无约束条件下,若考虑异方差,通过WLS计算得到的投资组合权重为:
在有约束,且考虑异方差时,通过WLS计算得到的投资组合权重为:
计算出投资组合权重后,根据公式
可计算因子收益率
通过以上截面回归方法计算出来的纯因子收益率虽然能够满足对暴露度约束的要求,但是由于投资组合中存在股票的权重为负值的情况,因而在A股市场并不具有可投资性。为了使计算出来的纯因子收益率更贴合国内市场,大家开始另辟蹊径,采用二次规划的方法来计算投资组合的权重。
二次规划的目标函数通常会根据用户目的的不同而有不同的设置,比如常见的目标函数有最大化目标因子暴露度、最大化投资组合夏普率、最小化投资组合跟踪误差、最小化投资组合全局风险等。由于这里我们是求解纯因子收益率,所以目标函数可以设置为最大化目标因子暴露度。
值得注意的是,上面的约束条件仅能满足对其他因子暴露为0个单位,而对指定因子的暴露并没有严格约束为1个单位。若要满足对指定因子的暴露度约束,则可以添加以下公式:
更近一步,如果我们想对行业进行中性化处理,对行业不做任何暴露,则可以添加以下公式:
当然,我们可以根据需要不断地增加约束条件,但需要注意的是,当约束过多的时候,有时无法得到收敛的解。
其中,
表示指定因子投资组合中的股票权重向量,
表示指定因子的暴露度向量,
表示其他因子的暴露度矩阵,
表示投资组合中股票的行业因子哑变量矩阵,
表示基准组合对应的行业权重向量。
纯理论的东西终于啰嗦完了,下面进入实操环节。构建因子暴露度矩阵构建风格因子暴露度矩阵构建行业因子暴露度哑变量矩阵构建国家因子暴露度向量
整合三者,得到最终的因子暴露度矩阵
2. 构建权重调整矩阵
3. 构建约束矩阵
4. 根据截面回归的结果计算权重矩阵
5. 根据公式计算因子收益率
6. 反向验证计算结果准确性
其中每一行表示该行所在因子在其他因子上的暴露。可以发现:国家纯因子组合对各行业纯因子都有暴露,对风格纯因子无暴露
行业纯因子组合100%做多本行业,100%做空国家因子
风格纯因子仅对自身有暴露,对任何其他因子暴露度为0
正如前文所述,WLS计算出来的权重包含负值,与A股市场难以做空的环境差异巨大,为此尝试通过优化器来改进这个问题。
为了简化流程,对前文生成的相关变量继续运用,唯一需要注意的是,下文的矩阵
仅表示风格因子,而非所有因子。同时为了避免冗余,这里直接粘贴所有代码,不再一步步运行返回结果。
# 特定风格因子的预期暴露度最大化
def func(w, X, loc=1):
'''w: 求解的最优权重[w1, w2, ..., wn]^TX: 投资组合个股风格因子暴露度矩阵H: 投资组合个股行业因子哑变量矩阵'''
return -np.dot(w, X[:, loc])
num = len(all_ids) # 股票数目
w_ = np.random.random_sample(num) # 生成介于[0, 1]之间的随机数
init_w = w_ / w_.sum() # 随机初始化权重值
X = ss_style_data.values # 表示N只股票的风格因子暴露度矩阵
H = industry_data.values # 表示N只股票的行业因子哑变量矩阵
h = industry_weights.values # 表示基准指数对应的行业权重,与所选取的参照基准有关
bnds = ((0, 1), ) * num # 权重限定位于[0, 1]之间
res_lst = []
for loc in range(X.shape[1]):
print('-->', loc)
X_ = np.delete(X, loc, axis=1)
cons = (
{'type': 'eq', 'fun': lambda w: np.dot(w, X_)}, # 数值为0的向量,向量长度等于风格因子数目减1
{'type': 'eq', 'fun': lambda w: np.dot(w, X[:, loc]) - 1}, # 对本身的风格暴露度限定为1
{'type': 'eq', 'fun': lambda w: np.dot(w, H)- h}, # 对行业因子暴露度与基准一致,由于不能做空,所以无法确保暴露度为0
{'type': 'eq', 'fun': lambda w: sum(w)-1}, # 不支持卖空,所以权重之和等于1,其中权重大于0的约束放在参数bounds里面
)
res = minimize(func, x0=init_w, args=(X, loc), constraints=cons, bounds=bnds, method='SLSQP', options={'disp': True})
res_lst.append(res.x)
程序运行时,可以看到其返回如下信息:
Exit Mode 0表示结果收敛,目标函数最小值为-1,说明对指定因子的暴露度恰好为1(求最大值转化为了求最小值),符合我们的约束条件。
其中,每一行表示该行所在风格纯因子对应的不同股票的权重,可以发现很多股票的权重在约束条件下变为了0 。
若要绘制纯因子收益率的历史表现,仅需对以上代码进行ForLoop即可。
行文至此,又到了说结束的时候。大家如果觉得对自己有点用,欢迎点赞、收藏。
参考文献
【3】基于组合权重优化的风格中性多因子选股策略