文章目录
1 概要
单因子回测是量化投资领域中常用的一种策略评估方法,用于评估某一特定因子(例如市盈率、市净率、动量等)对股票或资产价格变动的预测能力以及其在投资组合中的有效性。
本文基于优矿平台详细介绍了如何进行单因子分析,包括寻找因子、构建因子、因子分析、组合构建、回测分析和附加值分析。
文中代码可以在优矿环境中复现。
2 市场中性Alpha策略介绍
- 市场中性的 Alpha 策略旨在利用资产的价格波动,而不受整个市场的整体涨跌影响
- 核心思想是通过同时买入和卖出相关联的资产,以达到对冲市场风险的目的,从而专注于捕捉资产之间的价差或其他价格不一致性所产生的 Alpha
2.1 理论基础
- 假设市场完全有效,根据CAPM模型有,Rs=Rf+βs∗(Rm−Rf)
- 市场并非完全有效,Jensen’s alpha:αs=Rs−[Rf+βs∗(Rm−Rf)]
- 股票的收益是受多方面因素影响的,经典的Fama French三因素就告诉我们,市值大小、估值水平、以及市场因子就能解释股票收益,而且低市值、低估值能够获取超额收益
- 假设我们已经知道了哪些因子能够获取超额收益,那么我们根据这些因子构建股票组合(比如持有低市值、低估值的股票)
- 持有组合,做空基准,对冲获取稳定的差额收益(alpha收益)
2.2 实例展示
# 假设构建的多头组合每天跑赢基准0.1%
data = DataAPI.MktIdxdGet(ticker='000300', beginDate='20130101', field='tradeDate,CHGPct', pandas='1').set_index('tradeDate').rename(columns={'CHGPct':'benchmark'})
data['portfolio'] = data['benchmark'] + 0.001
data.cumsum().plot(figsize=(12,5))
3 单因子回测流程
- 策略的核心在于找到如上图一样能够稳健跑赢基准指数的多头组合
- 寻找组合的核心在于找到驱使股票获得超额收益的因子
- 本篇从单因子角度详细讲解研究步骤与细节
3.1 寻找因子
- 符合经济、金融投资逻辑
- 数据来源:基本面、行情、分析师预期、大数据分析
- 作为例子:以市盈率PE为例
3.2 构建因子
- 公式:PE = 每股价格/每股收益,将上式同时乘以总股本数就是,PE = 总市值/净利润
- 合理性:总市值是日度变化数据,净利润则是季度数据,为了对比的一致性以及结合企业经营的实际情况,一般采用TTM值(Trailing
twelve months) - 投资理念:由于我们假定是低估值会获得超额收益,所以将因子调整为 EP = 净利润/总市值,这样因子值越大就代表越高的持仓权重
# 优矿里提供了400多个因子数据,将诸如TTM类似的标准化算法工程化,可利用DataAPI直接获取
pe = DataAPI.MktStockFactorsOneDayGet(secID=set_universe('HS300'),tradeDate=u"20160922",field=u"secID,tradeDate,PE",pandas="1").set_index('secID')
pe.head()
# pe.plot(figsize=(14,5))
3.3 因子分析
- 构建好EP因子之后并非直接可用,还需要将其转化为实际可用的信号(signal)
- 常见的因子处理:去极值winsorize、中性化neutralize、标准化standardize
- 通过上述处理过的因子,得到的信号分布更平滑、更合理
3.3.1 去极值
- 正态分布去极值:3σ原则
- 分位数去极值
import numpy as np
# 去极值winsorize
after_winsorize = winsorize(pe['PE'].to_dict())
pe['winsorized PE'] = np.nan
pe.loc[after_winsorize.keys(),'winsorized PE'] = after_winsorize.values()
pe.plot(figsize=(14,5)).legend(fontsize=14)
3.3.2 标准化
- 普通标准化:z-score
- 风格标准化:行业内的普通标准化
# 标准化standardize
after_standardize = standardize(pe['winsorized PE'].to_dict())
pe['standardized PE'] = np.nan
pe.loc[after_standardize.keys(),'standardized PE'] = after_standardize.values()
pe['standardized PE'].plot(figsize=(14,5))
3.4 组合构建
-
选股
-
整体分位数:从所有备选股中选择因子得分大于某个分位数
-
风格分位数:以行业为例,在同一行业中选择因子得分大于某个分位数
-
权重
-
等权重:最常见也最通用
-
市值加权:中证指数编制时的权重考虑因素就是市值
-
风格中性权重:权重由市值和股票所属行业在指数中的权重决定
3.5 回测分析
- 回测区间:2010年1月1日~2016年9月1日,基准为沪深300,中证800成分股,策略每20天换仓一次
- 因子选取:市盈率PE
- 因子处理:用到了去极值(winsorize)、标准化(standardize)处理
- 组合构建:整体分位+风格中性权重
import pandas as pd
import numpy as np
start = '2022-03-22'
end = '2024-03-22'
benchmark = 'HS300' # 策略参考标准
universe = DynamicUniverse('HS300') + DynamicUniverse('ZZ500')
capital_base = 10000000 # 起始资金
freq = 'd' # 策略类型,'d'表示日间策略使用日线回测,'m'表示日内策略使用分钟线回测
refresh_rate = Monthly(1) # 调仓频率
accounts = {
'fantasy_account': AccountConfig(account_type='security', capital_base=10000000)
}
def initialize(context): # 初始化虚拟账户状态
pass
def handle_data(context): # 每个交易日的买入卖出指令
universe = context.get_universe()
yesterday = context.previous_date.strftime('%Y-%m-%d') # 向前移动一个工作日
data = context.history(universe, ['PE'], time_range=1, style='tas')
data = data[yesterday]
factor = data['PE'].dropna()
factor = pd.Series(winsorize(factor, win_type='QuantileDraw', pvalue=0.05)) # 去极值
factor = 1.0 / factor
factor = factor.replace([np.inf, -np.inf], 0.0)
signal = standardize(dict(factor)) # 标准化
# 组合构建
wts = simple_long_only(signal, yesterday)
# 交易部分
account = context.get_account('fantasy_account')
current_position = account.get_positions(exclude_halt=True)
target_position = wts.keys()
# 卖出当前持有,但目标持仓没有的部分
for stock in set(current_position).difference(target_position):
account.order_to(stock, 0)
# 根据目标持仓权重,逐一委托下单
for stock in target_position:
account.order_pct_to(stock, wts[stock])
3.6 附加值分析
- 找到效果比较好的单因子之后,就需要考虑单因子对已有模型的贡献
- 从本质上来说还是多因子合成,构建多因子来看整体效果是否有提高
- 下面的例子将市盈率和对数流通市值等权合成后的整体效果
start = '2022-03-22'
end = '2024-03-22'
benchmark = 'HS300' # 策略参考标准
universe = DynamicUniverse('HS300') + DynamicUniverse('ZZ500')
capital_base = 10000000 # 起始资金
freq = 'd' # 策略类型,'d'表示日间策略使用日线回测,'m'表示日内策略使用分钟线回测
refresh_rate = Monthly(1) # 调仓频率
accounts = {
'fantasy_account': AccountConfig(account_type='security', capital_base=10000000)
}
def initialize(context): # 初始化虚拟账户状态
context.signal_generator = SignalGenerator(Signal('PE'),
Signal('LCAP'))
def handle_data(context): # 每个交易日的买入卖出指令
universe = context.get_universe()
yesterday = context.previous_date.strftime('%Y-%m-%d') # 向前移动一个工作日
data = context.history(universe, ['PE', 'LCAP'], time_range=1, style='tas')
data = data[yesterday]
factor = data['PE']
factor = pd.Series(winsorize(factor, win_type='QuantileDraw', pvalue=0.05)) # 去极值
factor = 1.0 / factor
# factor = factor.replace([np.inf, -np.inf], 0.0)
signal_pe = standardize(dict(factor)) # 标准化
factor = data['LCAP']
factor = pd.Series(winsorize(factor, win_type='QuantileDraw', pvalue=0.05)) # 去极值
factor = 1.0 / factor
# factor = factor.replace([np.inf, -np.inf], 0.0)
signal_lcap = standardize(dict(factor)) # 标准化
# 信号合成
signal = (0.5*pd.Series(signal_pe)).add(0.5*pd.Series(signal_lcap), fill_value=0.0)
# 组合构建
wts = simple_long_only(dict(signal), yesterday)
# 交易部分
account = context.get_account('fantasy_account')
current_position = account.get_positions(exclude_halt=True)
target_position = wts.keys()
# 卖出当前持有,但目标持仓没有的部分
for stock in set(current_position).difference(target_position):
account.order_to(stock, 0)
# 根据目标持仓权重,逐一委托下单
for stock in target_position:
account.order_pct_to(stock, wts[stock])
可以看到,估值因子PE叠加市值因子LCAP后,组合alpha从6.3%降低到了6.0%,组合收益波动率从15.5%提高到了16.5%,IR从1.61降到了1.24,两种因子叠加后组合的预期收益略微降低,并且增加了组合的收益波动率,同时IR的下降表明投资组合的超额收益相对于风险的表现有所下降,这意味着估值因子PE叠加市值因子LCAP后并未产生更大的增量效应,因子是否具有附加值还依赖于投资者的主观判断和偏好。
参考文献
《多因子策略白皮书》——Datayes优矿