menu

股票多因子模型实战

代码: http://www.broadview.com.cn/40875

1. 概述

1.1. 什么是量化投资

量化分析师需要具备三方面的知识:金融、数学和计算机,而这也就是量化投资的“三轮驱动”,如图11所示。金融是量化分析师对金融市场、经济环境的理解,是构建量化模型的基础;而数学和计算机则是工具,是量化投资者用于表达、验证自己观点和策略的方法。

主流的量化方向:

  • 股票多因子
  • 量化CTA 通过计算一系列量价数据生成交易信号,进行多策略、多品种组合,以获得较平滑的净值曲线。
  • 套利
    • 统计套利 基于统计规律进行套利策略的实现,其背后的逻辑很简单:如果两个资产的价差或者比价在历史上一直存在均值回复的特征,而且历史数据经得起统计方法的检验,例如两者存在协整关系,那么后续就可以在价差偏离的时候进行反向操作,从而在价差回归的时候获利。
    • 内外盘套利 有很多商品在内外盘都有交易,例如黄金、大豆等。当内外盘价格在考虑汇率、关税、运费等因素之后依然有较大差异时,就可以进行内外盘套利。当内盘价格过高的时候,做空内盘商品,做多外盘商品;反之亦然。

1.2. 股票多因子模型框架

1.2.1. 基本概念

“因”就是起因,或者说原因,“子”则是分割后所得最小部分的意思,合起来理解,“因子”就是原因分割后的产物,而“股票多因子”的字面含义就是股票上涨/下跌的原因的组成部分。

一般来讲,股票的因子可以是直观可理解的“原因”,比如公司所在的行业、公司的盈利情况,以及一些技术指标等。在这样的思想之下,我们购买一系列股票的本质其实就是购买一系列驱动着这些股票上涨/下跌的因子。

如果我们吃了这种巧克力100克,那么在常规思维下,我们就是摄入100克巧克力。但是,在因子思维下,我们吃的并不是巧克力,而是2134千焦的能量、5.5克的蛋白质、26.9克的脂肪、59.2克的碳水化合物、203毫克的钠及其他没有被写进表里面的成分。在上面这个例子里,我们通过营养成分表把100克巧克力分解成了5个因子。这看起来似乎没有什么神奇之处,反而使简单问题复杂化了:原本摄入100克巧克力是比较好理解的,但使用因子思维就需要考虑5个因子了。但如果我们后续又吃了各种不同牌子的巧克力、蛋糕、苹果派等多种食物,那么在因子思维下,我们仍然只摄入了上面5个因子,这个时候,就是“化繁为简”了。同样地,对于我国沪深两市的数千只股票,甚至全球上万只股票,如果我们引入因子思维,那么也可以“化繁为简”。

一只股票的涨跌可以分成两个部分:股票自身的特性当时大盘整体的表现。例如,某一天上证指数上涨1.5%,而上证指数中的股票A上涨1.7%,股票B上涨1.2%,这个时候我们就可以认为,两只股票自身特性所带来的收益分别是0.2%(1.7%-1.5%)、-0.3%(1.2%-1.5%),而两只股票共有的大盘表现就是1.5%。在上面的例子中,我们将股票的收益率进行了简单的分解:由股票自身特性所带来的收益称为Alpha收益,而归属于大盘整体表现的收益,称之为Beta收益。当然,Alpha收益和Beta收益都是可正/可负的。通俗来讲,所谓的Alpha收益就是跑赢了大盘多少个点。

$R_{it} = {Alpha}_{it}+ {Beta}_t$

$\displaystyle P_t = \sum^{n}{k = 0}{w{it}{Alpha}_{it}} + {Beta}_t$

$P_t$是股票组合的整体收益率,$w_{it}$为某一时刻股票组合中股票i的权重,${Alpha}_{it}$为t时刻股票i的Alpha收益率,${Beta}_t$为t时刻的大盘收益率。整个组合超过大盘的部分就是整个组合的Alpha收益率。

对于Beta收益,也就是大盘的涨跌,投资者可以通过卖空股指期货的方式进行对冲。即我们可以不考虑大盘的涨跌,只要组合具有正的Alpha收益,那么即使整体收益率是负的,也可以获得不错的收益。当然,股票多因子模型也可以不使用股指期货做空,而仅利用多因子模型进行股票的选择和权重的配置,从而对指数收益进行增强。

例如我们持有某一个股票组合,股票全部来自中证500指数。在某一年的牛市中,我们的组合上涨了55%,中证500指数上涨了53%。那么我们在持有组合的同时,在股指期货上做空中证500股指期货,先不考虑升水、贴水等其他交易成本的影响,股指期货给我们带来53%的亏损,则最终的收益是55%53%=2%。这样我们就剥离出来了Alpha收益。或许有的读者觉得2%过少,但是如果我们将场景切换为熊市,市场下跌53%,这个时候只要Alpha收益为正,最后的收益就是正的。

$R_{it}=α_i+β_{1i}f_{1t}+β_{2i}f_{2t}+…+β_{ki}f_{kt}+ξ_{it}$

上面式子对股票收益率的分解更加细化。式中,$R_{it}$是股票i在t期的收益率,与之前一致。$α_i$则代表收益率的常数项,通常这一项数值极小,趋近于零。$β_{ki}f_{kt}$是因子k在$R_{it}$上的收益率分解。其中,$β_{ki}$是股票i在k因子上的敞口,也就是股票的该因子的因子值,$f_{kt}$是k因子的因子收益率。所以股票的收益率并不能完全被我们所知道的因子解释,所以会有一个残差项$ξ_{it}$。

1.2.2. 实践框架

多因子模型的具体构建分为3个子部分: 单因子测试与筛选、因子合成和组合构建。在进入实盘之后,还应当增加“归因分析”的部分。本书由于篇幅有限,只讨论前面三个部分。

  • 第一部分是单因子测试与筛选 先构建自己的因子池,并通过测试模板和特定的指标不断地测试这些因子,把目前市场下可行、有效的因子纳入入选因子池,这个过程就叫作“因子筛选”,这一过程以因子池和因子测试为基础。因子测试与筛选的详细内容将会在本书第4章介绍。

  • 第二部分是因子合成 对上一步筛选出来的因子进行加权求和变成一个合成因子。但其加权的过程有很多种方法,有传统的指标加权方法,也有比较复杂的机器学习和深度学习的加权方法,还可以完全不依赖于数量模型,单纯依靠投资者对当前市场的理解和经验进行加权,甚至可以将多种模型相结合。

  • 第三部分是组合构建 当我们通过一定的手段获得合成因子后,其实就获得了每一只股票的一个总的因子分值。我们可以按照从小到大的顺序给股票池中的股票排序,接下来根据合成因子的打分来计算哪只股票买多少比例,这就是股票组合的构建,如图1-5所示。

1.2.3. 调仓

假设现在是T时刻,而我们的调仓周期是t,那么依赖于现在可以获得的数据计算出T时刻的合成因子值,并通过组合构建方法计算出一个组合,记为$P_T$,如表13所示。

调仓周期可以是数分钟也可以是数个交易日甚至是数个月。在一个调仓周期(t)后,我们可以依赖新的数据计算出T+t时刻的股票组合,即$P_{T+t}$,如表14所示。

T+t时刻的新的组合可以与前一个组合进行轧差,在对比差异之后,进行调仓,确认需要买入和卖出的股票数量,如表15所示。如果调仓方向为负数,那么就是卖出特定比例的股票;如果为正数,那么就是买入特定比例的股票,最终使得组合$P_T$变成$P_{T+t}$。

1.3. 量化的基本问题

  • 因果性与相关性 人们很容易把相关性误认为是因果性。如果事件A是事件B发生的原因,事件B是事件A发生的结果,那么我们就可以说这两个事件具有因果性;而如果是事件C导致了事件A和事件B的发生,或者出于某种巧合导致事件A和事件B共同发生,那么这两个事件就不是因果关系,而可能是相关关系。
  • 幸存者偏差 我们在做量化分析的时候,就要特别注意是否犯了这个错误。比如使用当下可交易的全体股票作为股票池进行回测分析,这就是一个典型的幸存者偏差问题。回测过程中的每一个时间点都应该使用当时存在的股票作为股票池,而不是使用现在幸存下来的股票作为股票池。当我们的量化分析过程中存在幸存者偏差的时候,收益率往往会被高估。
  • 未来信息 所谓的未来信息就是在分析的时候,在某个时间节点上使用了这个节点还没有生成或者尚且不能获得的数据。如图1-6所示
  • 过度拟合与欠拟合 过度拟合的模型表面上完全拟合了样本内的数据集,在有限的样本量下模型准确度显得极高,但实际上模型的外推能力极差。过度拟合其实是将样本数据中含有的“噪声”也用于模型构建了,也就是对样本中的信息过度解读了。过拟合问题是常见的错误。 欠拟合则是过拟合的对立面。当我们的模型欠拟合的时候,往往拟合能力较差,没有充分使用样本中蕴含的信息。

    图1-7(a)是欠拟合的例子,其原本是一个指数的模型,但是当我们只考虑其背后线性部分的时候,就只能得到一个欠拟合的线性模型。这一线性模型在现有样本中的拟合度就显得不是很高,而在进行外推的时候,预测能力也明显不足。图1-7(c)则是过度拟合的例子,其在样本内完全拟合了每一个样本点,但是很显然,其外推能力极差,没有任何预测能力。图1-7(b)的情形则是一个较为合理的拟合情况,较好地反映了样本的信息,在样本内的拟合程度和样本外的预测能力上都有不错的表现。

    在金融数据建模中,通常我们会使用一些技巧来避免过度拟合。其一,模型的建立必须基于逻辑,即模型建立的背后逻辑必须与金融经济含义相匹配,而不仅是单纯的数据挖掘。这是金融数据建模与其他数据建模不同的地方。在金融数据建模过程中,即使模型表现得很完美,如果没有经济逻辑的支持,那么它也不是一个合格的模型。其二,可以使用常规的方法来测试模型是否过度拟合,如利用参数的敏感性测试来衡量模型是否过度拟合。如果我们在对参数进行小幅度调整后,发现整个模型的预测和拟合能力都出现了很大的变化,那么模型往往是过度拟合的,我们应该考虑简化模型。

  • 其他问题
    • 数据质量
      • 准确性
      • 及时性
    • 操作风险
      • 数据与模型的备份。
      • 对于常规数据和代码均应进行访问权限设置与防拷贝系统的部署。

2. 量化的python基础

3. 量化的概率统计基础

3.1. 分布的四个矩

  • 期望
  • 方差
  • 偏度
  • 峰度 对于随机变量,具体的峰度计算公式如下: 如图3-4所示,图中3个分布的方差、均值、偏度均一致,但是分布①的峰度最大、分布②的峰度其次、分布③的峰度最小。我们可以看到,分布①的肥尾特性最大,具有最长的肥尾。

正态分布的峰度则是一个比较有意思的话题。我们在之前讨论过,有些分布是尖峰肥尾的。那么问题就产生了,尖峰是和谁比较?肥尾又是和谁比较?答案就是正态分布。如果某一个分布与具有相同期望和标准差的正态分布相比,两端的尾部显得相对肥厚,那么我们就称其为肥尾分布。

峰度值有两种,其中一种是Python计算出来的超额峰度。我们看到,在计算正态分布的峰度值时,Python返回的是0。也就是说,当我们把正态分布的峰度作为一个基准时,如果其他分布峰度值大于0,那么该分布就是一个尖峰肥尾分布。根据峰度计算公式,正态分布也有峰度。这里要提醒读者,当我们讨论峰度的时候,一定要先弄清楚是超额峰度还是实际峰度。本书后面章节中讨论的峰度,若没有明确说明,则均指超额峰度。

3.2. 线性回归

3.2.1. 单元线性回归

如果模型中解释变量的个数只有一个,则叫作单元线性回归;如果有多个,则叫作多元线性回归。

import statsmodels.api as sm 
# 假设线性回归有常数项
results = sm.OLS(zgpa_beta_df['stock_daily_return'], sm.add_constant(zgpa_beta_df['market_daily_return']), hasconst=True).fit()
# 假设线性回归没有常数项
results = sm.OLS(zgpa_beta_df['stock_daily_return'], zgpa_beta_df['market_daily_return'], hasconst=False).fit()
results.summary()

这里对中国平安的每日收益率和市场整体的每日收益率做回归, 看中国品安在市场因子上的暴露。

第一个值得关注的指标是R-squared,也可以写作$R^2$,这个指标的学名叫作可决系数(coefficient of determination)。其背后的实际含义是解释变量在多大程度上解释了被解释变量。例如在上面的回归结果中,$R^2$是25.4%,那么中国平安这只股票的涨跌幅波动可以被大盘的涨跌幅波动解释掉25.4%

“coef”这一列是回归方程的系数,通过这一列我们就能得到回归方程,其中const代表的是常数项,余下的是解释变量的回归系数。 对于线性回归来说,这里的原假设就是我们的自变量与因变量没有关系,也就是斜率为0。如果原假设不成立,那么我们就比较有信心来使用计算出来的斜率,回归模型就可以进一步使用;如果原假设成立的概率超过预设值,那么我们的回归模型在统计意义上就是不正确的。 “P>|t|”这一列就是告诉我们原假设的检验结果。譬如,const这一行中“P>|t|”的数值是0.158,说明有15.8%的可能性原假设成立;而market_daily_return这一行是0,也就是说,原假设成立的概率是0。那么,常数项与被解释变量无关的假设很有可能是成立的,所以我们不能拒绝原假设

3.2.2. 多元线性回归

  • 对波士顿房价做多元线性回归
results = sm.OLS(house_df['MEDV'], house_df[['CRIM','ZN','RM','AGE','DIS','PTRATIO','B']], hasconst=True).fit()
results.summary()

在多元线性回归中, 我们发现,回归结果除了会给出R-squared,还会给出Ajd.R-squared,即调整后的R方。在上面的回归结果中,Ajd.Rsquared值为0.641,比0.646要小一些。那么在有了Rsquared之后为什么又有了调整后的Rsquared呢?调整后的Rsquared又调整了些什么呢?Rsquared是回归模型解释力度的衡量指标,如果增加解释变量,则显然是可以增加模型的解释力度的。这和直觉一致,更多的因素必然可以更好地解释现象。即使解释力度不增加,新增加的解释变量也不会使得原模型的解释力度下降。因此,如果我们仅观察Rsquared,那么在建立模型的时候,建模者就会过多地放入一些解释变量让模型的Rsquared尽可能地高。很显然这样的做法是违背常理的,于是便有了调整后的Rsquared,用于对模型中解释变量的个数进行惩罚。

除了调整后的R-squared,对于多元回归模型我们还需要考虑另外一个指标,就是F检验,其在statismodel的结果中以F-statisticProb(F-statistic)的名称给出。 按照我们之前对假设检验的讨论,F检验必然也对应一个原假设。多元回归模型中F检验的原假设是回归模型中所有的系数都为零。 很显然,如果原假设不能被拒绝,那么这一多元线性模型就是不能被采纳的。在这一例子中,我们看到Prob的数值极小,说明我们可以拒绝原假设,即模型通过F检验下,这一房价的多元线性模型是成立的。

  • 求上证50前十大重仓股对指数走势的解释度
result = sm.OLS(sz50_regression_df['daily_return'], sz50_regression_df[weight_stock_code_list], hasconst=True).fit()
result.summary()

这里求出的解释度为98%, 但是如果这10只股票的收益率高度相关,那么虽然我们是用10个变量在进行回归,但是背后的驱动因素可能是同一个。因此很有必要查看一下这10只股票之间的收益率相关性矩阵

sz50_regression_df.corr()

可以看到,这10只股票之间的相关性非常高,相关性数值普遍高于0.65。这说明之前的回归模型并不可靠,或者说,变量存在“多重共线性”。所谓的多重共线性就是多个解释变量之间的相关性较高,造成回归模型可信度的下降。 方差膨胀系数(Variance Inflation Factor,VIF)。方差膨胀系数是衡量多元线性回归模型中多重共线性严重程度的一种度量方法,计算公式如下:

$ {VIF}_i = \frac{1}{1-{R_i}^2} $

$R_i$为自变量i对其余自变量做回归分析的R-squared的算术平方根。很显然,VIF越大,说明该自变量可以被其余自变量解释的力度越大。通常,如果VIF大于5,就可以认为自变量存在较大的多重共线性。当然,这里的这个阈值可以根据使用过程中的实际情况进行调节, 使用python计算vif如下:

from statsmodels.stats.outliers_influence import variance_inflation_factor
vif = pd.DataFrame()
vif["VIF Factor"] = [variance_inflation_factor(np.array(sz50_regression_df[weight_stock_code_list]), cnt) for cnt in range(10)]
vif["features"] = weight_stock_code_list

计算结果如下:

3.2.3. 哑变量

类似于枚举, 将不可量化的分类转换成数值。

# 将行业代码列转换成哑变量
industry_dummies = pd.get_dummies(beta_df['ind_code'])
ind_beta_df = pd.concat([beta_df,industry_dummies], axis=1)

def regression_adj_r_square(one_day_trading_df):
    result = sm.OLS(one_day_trading_df['stock_daily_return'], one_day_trading_df[list(ind_beta_df.ind_code.unique())+['market_daily_return']], hasconst=False).fit()
    return result.rsquared_adj

ind_r_square = ind_beta_df.groupby('data_date').apply(lambda x: regression_adj_r_square(x))
ind_r_square.mean()

我们用这一回归函数,对ind_Beta_df进行逐日回归,回溯了2019年的数据,发现行业可以解释的股票涨跌幅比例约为23.6%

3.3. 业绩评价指标

3.3.1. 年化收益率

根据第四季度的日化收益率, 可以计算出该基金第四季度的累计收益率为6.52%。根据第四季度的收益率,计算其年化收益率为28.7%。

3.3.2. 夏普比率

夏普比率(SharpeRatio)是一种最常见的业绩评价指标,也是一个可以同时对收益与风险加以综合考虑的经典指标。

$ SR = \frac{P_r - R_f}{σ_P} $

SR为我们希望计算的夏普比率;$P_r$为计算期间投资者持有的标的组合获得的年化收益率;$R_f$为年化的无风险利率,通常可以采用十年期国债或者三个同Shibor利率;$σ_P$为年化后的组合收益率波动,也就是组合收益率的标准差。

3.3.3. 信息比率

信息比率(Information Ratio,IR)是一个衡量组合超额收益的指标,其含义是单位主动风险所带来的超额收益。

$IR = \frac{P_r - B_r}{TE} $

IR为信息比率,Pr为组合的收益率,Br为比较基准的收益率,TE(Tracking Error)是投资周期中每天的Pr与Br之差的标准差。比较基准一般根据情况而定,可以是某个大盘指数或者某个行业指数。

我们根据之前给出的基金的收益率和沪深300的收益率来计算一下信息比率,其中以沪深300作为比较基准,通过计算可以得到年化的Pr和Br,分别是28.7%和33.0%。而对于跟踪误差,仅需要计算基金收益率与基准收益率每天差值的标准差并年化即可,实际计算后得到结果为0.020,从而计算信息比率为:

4. 单因子测试

如果把股票的多因子模型比喻为做菜,那么第2章讨论的编程技术和第3章讨论的概率统计知识则可以比作做菜的锅碗瓢盆,而本章重点介绍的单因子则是制作这一道菜的原材料。

4.1. 因子的来源

4.1.1. 财务因子

  • 估值因子 PE、D/P、PB、PS、PEG
  • 盈利因子 上市公司的核心竞争力就是盈利能力,这是毋庸置疑的。通常我们会使用ROE、销售净利率这些因子来衡量上市公司的盈利能力,进而构造出盈利类因子。
  • 营运因子 通常我们衡量一家上市公司的营运能力会使用存货周转率、总资产周转率等指标。同样地,这些指标在计算后可以被直接作为因子
  • 成长因子 成长因子刻画的是上市公司业绩增长的速度,常用的指标有EPS的同比增长、ROE的同比增长、营业额的同比增长、利润总额的同比增长等。
  • 现金流因子 刻画公司现金流情况的因子有现金流稳定性因子、经营现金流质量因子等。

4.1.2. 分析师一致预期因子

  • EPS一致预期变动 利用分析师一致预期数据的最简单的方法就是比较分析师预测出来的EPS数据与已经实现的EPS数据。如果分析师一致预期的EPS远高于当前的EPS,那么说明市场对这一公司的盈利预期很高,往往有较多投资者看好。反之,则说明公司处于下行通道,股价往往受挫。
  • 评级变化率 在国内,卖方分析师的研究报告往往会对股票做出买入、增持、中性、减持或卖出这样的评级,例如之前提到的研报最后就有相应的评级说明。在图4-1中,我们可以看到分析师给出的评级是买入,对照报告最后的评级说明(如图43所示),可以知道“买入”这一评级的具体含义。

  • 分析师热度变化率 市场上的卖方分析师的数量在短时间内不会有特别大的变化,所以在较短时间内研究报告的产出数量基本是固定的。如果某只股票的研究报告突然多了起来,就说明卖方分析师们越来越关注这只股票了。基于这样的逻辑,我们可以针对每一只股票计算其某一段时间发布的研究报告数量总和的环比变化率,并据此来构建“分析师热度变化率因子”。

4.1.3. 技术因子

  • 动量因子 最常用的技术因子是“动量因子”,即过去特定时期的累计收益率,例如可以将每一只股票过去一年的收益率作为股票的动量因子。这样的因子之所以有效,是因为资金存在趋势追踪的特点,市场也往往有强者恒强的特性。
  • 乖离率BIAS 乖离率BIAS的计算公式为:BIAS=[(当日收盘价N日平均价)/N日平均价]式中的N通常取值为6、12或24。
  • 换手率因子 通过成交量也可以计算许多因子,例如换手率因子等。

4.1.4. 其他因子

  • 另类数据因子 例如论坛数据
  • ESG因子 ESG是“Environment、SocialResponsibility、CorporateGovernance”的缩写,指环境、责任和公司治理。ESG因子目前在海外比较流行,而在国内则较少被提及。ESG因子背后的逻辑是公司的价值有一部分会通过对环境的保护、对社会的外部性和公司内部治理的合理性所体现出来。

4.2. 大小盘因子

又称为市值因子, 观察图44的曲线我们发现,中证500走势会比上证50的走势强势 由此我们可以发现,市值的大小对股票的涨跌是有较大影响的,这也是为什么会有“盘子太大,涨不起来”这一说法的原因

在定义市值因子的时候,一般会去对市值取对数。因为股票在这个因子上的差别往往是指数级的,但实际差异确是线性的。


三个数据的概念:

面板数据:在数据集合中既有同一时刻下多个不同个体的数据,也有单个个体在不同时刻下的数据。

如果单独把面板数据中某一个时间点的数据拿出来,形成只有一个时刻、多个个体的数据集,那么这个数据集就叫作截面数据

如果我们只考察某一只个股,那么获得的数据就是时间序列数据

无论是异常值的去除还是因子值的标准化,都在一个时间点的截面数据中,将所有股票的因子值作为一个数据对象进行处理。因子模型的本质就是在某一个时刻对股票打分,并通过买入得分高的股票,卖出得分低的股票来获利。


因子处理的3个步骤:去极值与异常值、标准化、中性化,并分别介绍这些步骤的含义与作用。

  • 去极值与异常值 在这里当因子值大于或小于某一个阈值的时候,我们就将该因子值设为这一阈值,这一过程体现在分布图上就类似于在两端进行压缩的效果。例如我们将3倍标准差设为市值因子的阈值。在获得了因子值的上下限之后,当因子值大于上限值时,我们将因子值强行修改为上限值;当因子值小于下限值时,我们将因子值强行修改为下限值。

  • 标准化 标准化的作用就是去除因子的量纲,让每一个因子之间都可以进行比较、相互叠加。根据之前介绍的多因子模型框架图,最后这些单因子是需要进行组合的,而因子合成的前提就是这些因子具有相同的量纲。解决这一问题的方法就是对因子进行标准化。对因子进行标准化之后就不再有“单位”的概念,而仅是一个被标准化之后的分值(Z-score)

$x’ = (x-μ)/σ $

x′为标准化后的因子值,x为因子的原始值,μ是因子的均值,σ是因子值的标准差。

  • 中性化 中性化的数学原理其实很简单,就是将行业变成哑变量后再作为解释变量,而需要中性化的因子则作为被解释变量进行回归。回归的残差项就是被中性化后的因子值。如果用行业哑变量来解释因子值,那么不能被行业解释的那一部分就是剔除了行业因素后的因子值。而剔除了行业因素的因子值就是回归模型的残差项,这也正是我们希望获得的行业中性化之后的结果。
## 去极值与异常值 ##
sub_trading_data = trading_data_2019[trading_data_2019['data_date'] == '2019-05-27']
sub_trading_data['size'] = np.log(sub_trading_data['mv'])
size_upper = sub_trading_data['size'].mean() + 3 * sub_trading_data['size'].std()
size_lower = sub_trading_data['size'].mean() - 3 * sub_trading_data['size'].std()
sub_trading_data['size'] = sub_trading_data['size'].where(sub_trading_data['size'] < size_upper, size_upper).where(sub_trading_data['size'] > size_lower, size_lower)
sub_trading_data['size'].hist(bins=100, figsize=(18, 9))

## 标准化 ##
sub_trading_data['size'] = (sub_trading_data['size'] - sub_trading_data['size'].mean()) / sub_trading_data['size'].std()

## 中性化 ##
def industry_neutralization(factor_df, factor_name):
    result = sm.OLS(factor_df[factor_name], factor_df[list(factor_df.ind_code.unique())], hasconst=True).fit()
    # 返回回归模型的残差项
    return result.resid

# 获取行业哑变量
sub_trading_data = pd.concat([sub_trading_data, pd.get_dummies(sub_trading_data['ind_code'])], axis=1)
sub_trading_data['size_factor_neuted'] = industry_neutralization(sub_trading_data, 'size')
sub_trading_data[['data_date', 'secucode','ind_code','size', 'size_factor_neuted']]

# 看银行业(较多大市值)在行业中性化之后的变化,从`2.2`处理后变成了`0`
sub_trading_data[sub_trading_data.ind_code == '480000'][['size', 'size_factor_neuted']].mean()
size                  2.269385e+00
size_factor_neuted   -8.187895e-16
dtype: float64

# 看化工行业(较多小市值)在行业中性化之后的变化,从`-2.2`处理后变成了`0`
sub_trading_data[sub_trading_data.ind_code == '220000'][['size', 'size_factor_neuted']].mean()
size                 -2.150850e-01
size_factor_neuted   -1.959217e-17
dtype: float64

4.3. ROE

ROE=最近12个月(TTM)净利润/(0.5×最新一期财报股东权益+0.5×上年同期股东权益)

我们将该公式计算出来的ROE因子称为TTM(Trailing Twelve Months,简称为TTM,即最近12个月)的ROE。这里选择12个月可以避免因为季节性的营收差异导致的误差

## 去极值与异常值 ##
roe_factor = roe_factor[roe_factor['data_date'] == '2019-05-27']
roe_upper = roe_factor['roe'].mean() + 3 * roe_factor['roe'].std()
roe_lower = roe_factor['roe'].mean() - 3 * roe_factor['roe'].std()

# 将超过阈值的值直接置为阈值
roe_factor['roe'] = roe_factor['roe'].where(roe_factor['roe'] < roe_upper, roe_upper).where(roe_factor['roe'] > roe_lower, roe_lower)
# 我们可以将ROE上边界设定为30%,下边界设定为0。当然,这里阈值的设定需要投资者融入自己的观点,甚至可以随着不同的市场环境的变化而变化。
roe_factor['roe'] = roe_factor['roe'].where(roe_factor['roe'] < 30, 30).where(roe_factor['roe'] > 0, 0)
# 画出处理后的直方图
roe_factor['roe'].hist(bins=100, figsize=(18, 9))

## 标准化 ##
roe_factor['roe'] = (roe_factor['roe'] - roe_factor['roe'].mean()) / roe_factor['roe'].std()

## 中性化 ##
def industry_and_size_neutralization(factor_df, factor_name):
    # 行业中性化以及市值中性化
    result = sm.OLS(factor_df[factor_name], factor_df[list(factor_df.ind_code.unique()) + ['size']], hasconst=False).fit()
    return result.resid

sub_trading_data = pd.concat([sub_trading_data, pd.get_dummies(sub_trading_data['ind_code'])], axis=1)
roe_neuted_df = pd.merge(sub_trading_data, roe_factor[['secucode', 'roe']])
roe_neuted_df['roe_neuted'] = industry_and_size_neutralization(roe_neuted_df, 'roe')

roe_neuted_df[['data_date','secucode', 'roe_neuted']]

4.4. RSI

RSI这一指标就是相对强弱指数,反映的是一定时期内某只股票的市场情绪。 RSI的定义较多,本书中采用如下的RSI计算方式: RSI=100×RS/(1+RS) 其中,RS为N天内股票收盘价上涨数之和的平均值与N天内收盘价下跌数之和的平均值的比值。通常N取14

因为涨跌为两天价格之差,所以表47中只有14天涨跌价格。该股票在前14天内的收盘价上涨金额之和:0.1+0.9+0.5+0.4+0.7+2+0.2+0.8+0.1+0.1=5.8元;14天内收盘价下跌金额绝对值之和:1.1+0.1+1.3+0.7=3.2元。那么RS=(5.8/14)/(3.2/14)≈1.81。RSI=100×(1.81/(1+1.81))的计算结果约为64.4。

# 计算累计收益率
trading_data_2019[trading_data_2019['secucode'] == '601318.SH'] .set_index('data_date')['daily_return'].add(1).cumprod().plot(figsize=(18,9))

def RSI_cal(rolling_ser):
    # 这里使用了价格指数化,传入的参数是一组日度收益率的数据。沿用之前的方法,先将收益率加1后进行累乘,然后乘以100作为价格指数序列
    price_ser = 100 * pd.Series(rolling_ser).add(1).cumprod()
    price_ser_diff = price_ser.diff(1).dropna()
    RS = sum([item for item in price_ser_diff if item > 0]) / sum([-item for item in price_ser_diff if item < 0])
    return 100 * (RS / (1 + RS))
# 计算14天内的RSI
trading_data_2019[trading_data_2019['secucode'] == '601318.SH'].set_index('data_date')['daily_return'].rolling(15).apply(RSI_cal).plot(figsize=(19, 9))

4.5. 其他因子的计算

4.5.1. BTOP因子

PE(利润收益率) PB(平均市净率) 用ETOP(PE的倒数)或者BTOP(PB的倒数)作为指标在实践中更常见。 BTOP的计算公式为:BTOP=最新一期不含少数股东权益的股东权益合计/公司总市值

4.5.2. ROE稳定性因子

式中,n为ROE财务数据的期数,${ROE}i$表示第i期的ROE值,$u{ROE}$表示各期ROE的均值。

4.5.3. EPS一致预期变动率因子

$EPS一致预期变动率=分析师一致预期{EPS}t/分析师一致预期{EPS}{t-3} - 1$

分析师一致预期${EPS}t$为当下分析师预期的EPS值,$分析师一致预期{EPS}{t-3}$为3个月前分析师一致预期的EPS值。若这一变动率为正,则说明当下的EPS预期好于之前,公司基本面有所改善或者进一步向好;反之,则说明基本面有所恶化。

4.5.4. 舆论因子

我们也可以从新浪微博等社交媒体处获取相关的股票讨论信息,然后经过文本情感分析算法,获得当下投资者对股票的情感信息,进而将其量化为因子。

4.6. 单因子的测试分析

在单因子测试这一部分,我们需要使用一个叫作Alphalens的工具。Alphalens是一个用于因子测试的Python第三方库,使用方便,功能齐全,经过之前流程处理后的因子值将会被送入这一工具完成单因子测试的流程。

4.6.1 单因子测试的基本逻辑

在多因子框架下,挖掘和计算因子的最终目的是希望因子能够准确预测股票的涨跌。顺着这个逻辑,我们先梳理一下因子测试的基本逻辑。最基本的思路就是把T时刻的因子值和T到T+t区间股票的收益率进行相关性分析。如果存在较高的正相关性,则说明较高的因子值可以获得较高的收益率,也就是因子具有较好的预测能力。

4.6.2 Alphalens简介

Alphalens提供了一个用于整合数据的强大函数(get_clean_factor_and_forward_returns),我们只需要按照其格式要求依次传入所需数据,就可以获得Alphalens可以接受的数据集合。 这个函数的输出就是后续其他分析函数的输入。

4.6.3 因子IC分析

$IR = IC * \sqrt{B * W}$

IR,即信息比率。在主动管理理论中有一个“基本法则”(FundmentalLaw) 公式中的B指的是交易(下注)的次数(bets),W是指交易的范围(width),两者相乘体现的是交易的广度(breadth)。 IC其实就是因子值和股票收益率在时间截面上的相关性。

一个组合的IR取决于3项:指导投资的因子的IC、交易的次数、每一次交易股票的数量。直观上来理解,如果我们的因子IC足够高,也就是预测能力足够强,那么少量的交易就可以保证这个高IC的因子效果能得以体现,且收益稳定;而如果我们的因子IC较低,那么就需要更多次交易或者更多的资产数量才能体现出这个因子的效果。

对于IC而言,投资者最希望的就是一个因子具有稳定的IC序列,均值高,方差小,t统计量大,pvalue小。

以1天调仓的Q-Q图为例(图4-23右图),观察一下图的左下角,可以发现正态分布均值减去3倍标准差的位置与我们观测的分布的均值减去3.5倍标准差的位置大约保持一致。在前面介绍正态分布的时候,我们提到过3倍标准差以内正态分布已经涵盖了99.74%的样本。由于正态分布是一个对称分布,也就是从均值到3倍标准差之间含有49.87%的数据,因此可以得出3倍标准差之外的数据占比为0.13%。但是对于我们观测的分布,在3倍标准差之外的数据占比一定是大于0.13%的,因为3.5倍左右的标准差倍数大约与标准正态分布的3倍分位数一致,所以,我们观测的分布在3倍标准差之外的数据量是较多的。也就是说,我们观测的分布存在肥尾现象。同样的分析方法也适用于分布的右侧。

下面介绍因子IC的季节性热力图。Alphalens会在每个自然月进行IC均值的求取,然后绘制成热力图。市值因子的热力图如图426、图427和图428所示。季节性热力图可以让我们观测因子的季节性和历年的有效性。例如,从热力图中我们发现,市值因子在每年的春节前后往往表现不佳,这可能与年底市场流动性不足有关。

问题: 如何计算因子的IR?

# 计算未来五天收益率和因子值的秩相关系数
spearman_ic = factor_data.reset_index().groupby('date').apply(lambda x: x['factor'].corr(x['5D'], method='spearman'))

Alphalens会有相关图表的输出。


4.6.4 收益率分析

alphalens.tears.create_returns_tear_sheet(factor_data, long_short=False, group_neutral=False,by_group=False)
  • long_short参数 用于设置Alphalens计算收益率的方法。若设置为True,则Alphalens将会采用多空组合收益率计算方法进行计算。所谓多空组合的收益率,就是在计算分组的收益率时扣除每组的平均收益率。若设置为False,则不扣除每组的平均收益率,直接使用原始收益率。
  • group_neutralgroup_neutral参数 用于设置是否对收益率进行行业中性化,也就是在计算收益率的时候是否减去行业均值。在信息系数分析的函数中,也有这一参数。由于我们的因子已经进行过行业中性化,所以将其设置为False。
  • by_groupby_group参数 用于设置是否在进行收益率测试的时候给出分行业测试的结果。

一个好的因子应该有较高的Alpha收益与较低的Beta属性。同时Top组和Bottom组之间的差值应该尽可能大。如果这些特性在多个调仓周期中都能吻合的话,就更加说明这是一个好因子了。

从分组收益率柱状图中可以很直观地看到,在不同的调仓周期中,因子的分组收益率是否都遵循单调向上的原则。一个好的因子,应该是因子值越高,其分组收益率越高。图429中的市值因子分组收益率柱状图就是一个不错的例子。

收益率小提琴图其实就是收益率均值柱状图的高级版,其中给出的不再是分组的收益率均值点,而是收益率的完整分布。例如图430中我们可以看到,最小的极端值约为400bps,最大值约为200bps,但是极端值的数量极少,大部分收益率集中于100bps~100bps。

问题: 假设某一天市场上只有5只股票,且因子值如表428所示,那么,这一天我们应该构建一个怎样的组合才能叫作因子加权组合呢?

很简单,就是按照因子值进行权重分配。权重的计算也很简单,先将所有的因子值取绝对值,然后求和,将这一总和作为分母。再以每个因子值作为分子,计算的结果就是这一股票在组合中的权重。例如,上述股票1的组合权重为2/(2+1.3+0.4+1+2)≈0.299;股票4的权重约为0.149。分别计算之后,我们就可以获得因子加权组合中每一只股票的权重。图431就是市值因子根据这个规则计算的因子加权组合的收益率曲线(5天调仓)。


除了上面的这种因子加权构建的方法,还有一种更简单的计算方法,就是分组后的每一组股票都成为一个独立的组合,以每天组合内个股收益率的均值作为该组合的收益率。这样,当其分为10组的时候,就会有10条收益率曲线,如图4-32所示。

一个好的因子通常在这里会体现出较强的筛选性,也就是分组的收益率曲线会分化,各自的差异不断体现出来。图形越发散,说明因子的效果越好,因子对股票的区分度越高。


利用分组中的Top组平均收益率与Bottom组平均收益率的差值可得到一个时间序列的图,同时对这一收益率差值进行一个月的移动平均,如图435所示。

收益率差值是一个很重要的指标。对于理想的因子,这个差值越大越好,这说明因子对于收益率不同的股票的区分度越高;此外,还需要收益率差值曲线保持方向上的稳定。

4.6.5 换手率

当我们考虑因子造成的换手率的时候,其实就是在考察因子的稳定性,也就是因子值每一期的变化。因子的自相关性越高,其值越接近1,说明这一因子是比较稳定的因子,造成的换手率必然不会很高;反过来,如果因子的自相关性极低,甚至是负相关性,则说明因子的波动较大,造成的股票换手率必然不低。

4.7 常见因子的测试结果

4.7.1 ROE测试结果

4.7.2 销售净利率

与ROE因子相比,这一因子的分组收益率单调性较差。这也与其较低的IC均值相吻合。

同样地,作为一个财务指标因子,其换手率也体现出很明显的突变:在新的财务报表公布的时候,会发生较大的换手率。

4.7.3 MAC10

MAC10是一个简单的技术指标,其公式为:MAC10=10日移动均价/今日收盘价

4.7.4 BTOP因子

第5章 因子合成

5.1 经典加权方法

5.1.1 等权

5.1.2 滚动IC与IC_IR

$IC_{w^t_f}=\frac{\sum^{n}_{i=1}{IC}^{t-i}_f}{n}$

式中,n为滚动IC的滚动计算长度,通常我们可以选取120个交易日或250个交易日;${IC}^{t-i}_f$为因子f在t-i交易时刻计算出来的因子IC值;$IC_{w^t_f}$为f因子在t交易时刻的滚动平均IC,将会被用来作为权重。

为了对因子IC的波动性进行惩罚,我们可以计算因子的滚动IC_IR并将其作为因子加权的依据。IC_IR的计算公式如下:

$IC_IR_{w^t_f}=\frac{IC_{w^t_f}}{std(IC_nK)}$

式中,为因子f在t交易时刻的滚动IC均值;$std(IC_nK)$为这一区间IC值的标准差。观察上面的公式可以发现,IC_IR加权与滚动IC加权之间的差别在于是否除以滚动计算区间IC值的标准差。

IC_df是各个因子的历史IC值。

计算$IC_{w^t_f}$和$IC_IR_{w^t_f}$如下所示:

IC_120_weight = IC_df.rolling(120).mean().dropna()
ICIR_120_weight = IC_df.rolling(120).apply(lambda x: x.mean()/x.std()).dropna()

5.1.3 合成因子测试结果

  • 收益率表
  • 分组收益率(看分组收益的单调性)
  • 因子值加权收益率
  • IC表 主要看绝对数值(IC Mean)、风险调整后的IR(Risk-Adjusted IC)、偏度(IC Skew)、峰度(IC Kurtosis)。
  • 分组换手率

5.1.4 其他加权方法

  • 机器学习 在实际的金融市场中,因子与股票的收益率之间可能并不是简单的线性关系。例如,因子A与因子B相乘会有更好的预测能力,甚至因子A在因子B的幂次下具有较好的预测能力。很明显,这是前面的加权方法所不能获取的信息。机器学习的引入可以增加这些非线性因素的表达,但是金融市场的特性与其他领域不同。大部分机器学习模型是一个黑箱,很难有直观上的理解,而这在金融领域很难被接受,所以一般的尝试者会更多地使用一些可以直观理解的机器学习模型,例如决策树、随机森林等。

  • 因子估值 我们衡量一个因子估值是低还是高的方法就是比较因子得分最高的一组和最低的一组之间估值的差距。例如,将ROE因子值最大的一组和最小的一组取出,计算两组股票的平均PB,然后做差,通过观察这一差值的时间序列来分析因子估值的高低。当这一因子的估值处于高位的时候,我们降低其权重;当估值处于低位的时候,增加其权重。通过这样的估值判断来调节因子权重。

5.2 情景配置(因子择时)

5.2.1 市值因子的分析

在前面讨论市值因子曲线的时候,我们提出了两个问题。

  • 为什么在2015年之后,这一因子的表现特别好?
  • 为什么在2017年之后,这一因子开始掉头向下了?

基于上面的分析,我们可以明确,影响这一因子表现的主要因素是:流动性的宽松、信贷政策、监管态度。根据这个思路,我们来观察一下2017年前后这些因素的变化情况。

  • 流动性的宽松 十年期国债收益率是全市场利率的一个重要的锚。值得关注的是,在2017年之后,国债收益率开始上行,也正是在这个时候市值因子开始反转。
  • 信贷政策 广义货币(M2),如图514所示。M2作为货币供应量的一个观测指标,可以用来衡量信贷政策的收紧与放松。可以看到,从2015年年末开始,M2的增速开始下行,到2017年之后跌至10%以下,这也与市值因子的表现一致
  • 监管态度 在2017年前后监管趋紧,包括资管新规、金融供给侧改革等,使市场整体处于金融去杠杆的环境中。

我们可以建立一个初步的市值因子权重选择框架,如表5-12所示。笔者认为流动性是市值因子三个驱动要素里面最为重要的一个。

5.2.2 ROE因子的择时

P = PE * E

式中,PE反映的是市场给予的估值,这与市场的信心、流动性等有关;E为上市公司的盈利。 那么,影响整个市场的主要因素就可以分为估值驱动与盈利驱动两种。如果是盈利主导的市场,则是E主导P的变动,这个时候投资者将会更加注重上市公司的盈利水平,ROE因子就会显得极为重要。而在估值主导市场的时候,也就是PE主导P的变动时,市场主要受到情绪、流动性或者监管态度的影响,这个时候ROE因子则不那么重要。

第6章 组合构建

在根据因子模型计算得到因子值后, 如何根据这个数值来构建投资组合?以什么样的权重选择哪些股票?

6.1 一般方法

6.1.1 等权加权

# 读取收益率数据
tpd = pd.read_hdf('total_tpd.h5', key='data')
tpd.sort_values(['data_date', 'secucode'], inplace=True)
# 去极值
tpd = tpd[(tpd.daily_return > -0.11) & (tpd.daily_return < 0.11)]
# 读取因子数据
alpha_df = pd.read_hdf('./factor/eaqul_weight_alpha.h5',key='data')
alpha_df.sort_values(['secucode','data_date' ], inplace=True)
# 这里选用了市值因子, 保证因子值和收益率的正相关
alpha_df.factor_neuted = alpha_df.groupby('secucode').apply(lambda x: x['equal_weight_alpha'].shift(-1)).values

def top_quantile(df, pct=0.05):
    up_line = df['equal_weight_alpha'].quantile(1 - pct)
    return df['equal_weight_alpha'] >= up_line
# 选取因子值在前`0.05`的股票
alpha_df['flag'] = alpha_df.groupby('data_date').apply(lambda x: top_quantile(x)).values
alpha_df = alpha_df[alpha_df.flag]
# 获取这些股票的收益率和市值数据
alpha_df = alpha_df.merge(tpd[['data_date','secucode','daily_return', 'mv']], on=['data_date','secucode'], how='left')
# 等权加权的收益情况
alpha_df.groupby('data_date')['daily_return'].mean().add(1).cumprod().plot(figsize=(16, 9))

6.1.2 市值加权

# 使用绝对市值加权
mv_weighted_portfolio = alpha_df.groupby('data_date').apply(lambda x: (x['daily_return'] * x['mv']).sum() / x['mv'].sum())
alpha_df.groupby('data_date')['daily_return'].mean().add(1).cumprod().plot(figsize=(16, 9))

# 使用市值开平加权
mv_root_weighted_portfolio = alpha_df.groupby('data_date').apply(lambda x: (x['daily_return'] * np.sqrt(x['mv'])).sum() / np.sqrt(x['mv']).sum())
mv_root_weighted_portfolio.add(1).cumprod().plot(figsize=(16, 9))

6.2 均值-方差组合

6.2.1 优化器的使用

解决二次规划问题的一般方法。

参考: https://cvxopt.org/userguide/intro.html

官网的例子:

【例1】

解法:

from cvxopt import matrix, solvers
P = 2*matrix([ [2, .5], [.5, 1] ])
q = matrix([1.0, 1.0])
G = matrix([[-1.0,0.0],[0.0,-1.0]])
h = matrix([0.0,0.0])
A = matrix([[1.0, 1.0]]).T
b = matrix([1.0])
sol=solvers.qp(P, q, G, h, A, b)
print( sol["x"])

输出:

 pcost       dcost       gap    pres   dres
 0:  1.8889e+00  7.7778e-01  1e+00  2e-16  2e+00
 1:  1.8769e+00  1.8320e+00  4e-02  1e-16  6e-02
 2:  1.8750e+00  1.8739e+00  1e-03  1e-16  5e-04
 3:  1.8750e+00  1.8750e+00  1e-05  0e+00  5e-06
 4:  1.8750e+00  1.8750e+00  1e-07  2e-16  5e-08
Optimal solution found.
Optimal solution found.
[ 2.50e-01]
[ 7.50e-01]

最优解x1=0.25, x2=0.75


科学的组合构建方法应在收益和风险之间取得平衡,用可以接受的风险去获得最大的收益或者在一定的预期收益下寻找最小风险的组合。显然,这一想法可以变成一个优化问题。

既然我们希望用更小的代价(组合风险)来获得希望达到的收益率,那么可以假设希望达到的收益率是$μ_p$,优化表达式可以写成下面这一形式:

$(min) σ^2 = w^T\sum{w}$ $(s.t.) w^Tμ=μ_p 且w^TI=1$

【例2】

# 设定预期收益率为10%
target_return = 0.1
# 假设三只股票的协方差矩阵如下,该数据可以从数据上例如Barra和Axioma购买
cov_matrix = np.array([[0.106, 0.04,0.014], 
          [0.04, 0.01,0.011],
           [0.014, 0.011, 0.03]])
P=2*matrix(cov_matrix)
q = matrix([0.0, 0.0, 0.0])

G = matrix([[-1.0,0.0],[0.0,-1.0]])
h = matrix([0.0,0.0])
# 先前给出的三只股票的预期收益率
A = matrix([ [0.04, 0.2, -0.1],[1, 1, 1]]).T
b = matrix([target_return,1])
sol=solvers.qp(P, q,A=A,b=b)
print(sol['x'])
# 输出优化组合的结果, 三只股票的权重是-24.7%/78.2%/46.5%
x = np.array([sol['x'][0], sol['x'][1], sol['x'][2]])
[-2.47e-01]
[ 7.82e-01]
[ 4.65e-01]

# 在这一权重下验证预期收益率
np.array([0.04, 0.2, -0.1]).T.dot(x)
# 在这一权重下验证预期组合的风险为9.2%左右
np.sqrt(x.T.dot([[0.106, 0.04,0.014], 
          [0.04, 0.01,0.011],
           [0.014, 0.011, 0.03]]).dot(x))
0.09166614659030055

6.2.2 “均值-方差”效用函数

$E = μ_p - Aσ_p^2$

式中,$μ_p$为组合的预期收益率;$σ_p$为组合的预期波动率,用于衡量风险;A为投资者的风险厌恶系数。