一、动量策略的一点历史
1.1 三大互补选股维度
1、Momentum:当价格沿着过去的轨迹继续运动时,我们能够获得收益;
2、Value:当价格恢复到之前的某种均衡状态时,我们能够获得收益;
3、Carry:当价格不发生变化时,我们能够获得收益。
注:假如我们买入一支股票并一直持有,如果股票价格不变,那么我们获得的收益就是该股票的分红。股息率(diviedend yield)就是Carry收益率。
1.2 动量策略的发展1967年,Robert Levy在Journal of Finance 发表了Relative Strength as a Criterion for Investment Selection 的文章(当年动量一词还未被造出);
1970s开始,有效市场假说(EMH)被提出并成为主流;动量现象的研究被极大地抑制。随着人们对市场理解的加深以及价值投资的巨大成功,EMH不再权威,但动量任然被认为是‘一种黑色艺术、一种巫术魔力’;
1993年,Jegadeesh和Titman在Journal of Finance 发表了一篇里程碑式的文章,题为Return to Buying Winners and Selling Losers:Implications for Stock Market Efficiency;
2008年,EMH之父Eugene Fama在美国金融协会的采访中承认了动量的存在;
2013年,Asness等人在Journal of Finance 发表了影响深远的Value and Momentum Everywhere。
1.3 Momentum和Value无处不在(Q1:下图哪里体现了呢?)
二、基础版动量策略
2.1 策略出发点
1、在中国股市中,反转强于动量,且海外流行的动量选股方法(即使用t-12月到t-1月之间的收益率选股)并不好使。
2、在构造动量因子时,应该注意其在市值因子上的暴露。(Q2:什么意思啊?)
2.2 构造动量因子
使用过去60个交易日风险调整后的涨跌幅作为动量因子(它和市值因子的相关性仅0.085),其计算公式如下所示:
60−3000∗ ∗ (Q3:3000是什么)
其中 60为60日涨跌幅,即60个交易日内的收益率; 为60个交易日内收益率的标准差,代表风险。
2.3 策略思想
1、回测期:2009年1月1日至2018年9月12日;
2、调仓日:按月调仓,每月末按收盘价买卖股票;
3、股票池:中证500成分股;剔除ST股票;剔除由于停牌等原因而无法买卖的股票;
4、交易模型:每月末更新动量指标并重新对股票排名,新股理想仓位为1%,等权配置;
将中证500成分股按因子排名分为5组,由于因子值越大越好,按由小到大排名,组别越大的组合越好,即G5组优于G1组;
单边交易费用为千分之一。
备注:由于按日调仓需提取的日度数据量较为庞大,此处且先采用按月调仓的策略来传递高质量动量选股思想和呈现选股效果。
2.4 策略实现
2.4.1 导入python库
2.4.2 提取调仓日期
先是提取2009年1月1日-2018年9月12日回测区间内的各个月末交易日期,再提取每月月末往前推60日的交易日期,以便后面计算回测区间内的各个月末交易日期对应的过去60日涨跌幅。
##提取区间内的月末交易日期
def get_trade_date(start_date, end_date, period='M'): #输入参数start_date为回测起始日期,end_date为回测结束日期,period='M'代表提取的是月末交易日期
data = w.tdays(start_date,end_date, period=period)
trade_dates = data.Data[0]
trade_dates = [dt.strftime("%Y-%m-%d") for dt in trade_dates]
return trade_dates
##设置回测区间
start_date='20090101' #回测起始时间
end_date='20180912' #回测结束时间
dates=get_trade_date(start_date, end_date, period='M') #提取区间各月末交易日
dates_bef60_=[w.tdaysoffset(-60,date,"").Data[0][0].strftime("%Y-%m-%d") for date in dates] #得每月月末往前推60日的交易日期
2.4.3 构建选股函数
先提取各调仓日的中证500成分股,再剔除ST和由于停牌等原因而无法买卖的股票,即保留中证500中非ST和处于交易状态的股票。
2.4.4 构建动量因子
先提取出各调仓日满足选股条件的股票对应的过去60日涨跌幅,再提取各股票过去60日的收益波动率,最后利用前面的因子计算公式计算动量因子。
##提取数据并计算动量因子
def get_momentum_factor(dates_bef60_,dates): #输入参数dates_bef60_为各调仓日前推60日的交易日期, dates为回测期内各个月末交易日(也就是调仓日)
dict_=OrderedDict() #创建每期存取数据字典
for (date_bef60,date) in zip(dates_bef60_,dates):
stocks=get_stocks(date) #提取各交易日的中证500成分股中满足筛选条件的股票池
#step1:计算股票过去60日的收益率
returns=w.wss(stocks, "pct_chg_per","startDate="+date_bef60+";endDate="+date,usedf=True)[1]/100#提取各股票过去60个交易日的涨跌幅,即为过去60日内的收益率
returns.columns=['return']
#step2:计算股票过去60日的收益波动率,即日收益率的标准差
sigma=w.wss(stocks, "stdevr","startDate="+date_bef60+";endDate="+date+";period=1;returnType=1",usedf=True)[1]/100 #提取股票过去60日的收益波动率
sigma.columns=['risk']
#step3:计算动量因子
data=pd.concat([returns,sigma],axis=1,join='inner') #将收益率与风险合并
data['momentum']=data['return']-3000*(data['risk']**2) #计算动量因子
print(date,'success')
dict_[date]=data
datas=pd.concat(dict_.values(),keys=dict_.keys())
datas.index.names=['date','codes']
return datas
datas=get_momentum_factor(dates_bef60_,dates) #此处数据提取需花费较多时间
datas.to_csv('data/basic_momentum_strategy_datas.csv') #将基础版动量策略的原始数据存入数据文件中,便于后期读取
2.4.5 按因子排名进行分组
由于动量因子越大越好,在对因子按升序排名并分组后,组号越大的组合表现越优,即G5>G4>G3>G2>G1。
#定义分组函数,将中证500成分股按因子值排名分为5组
def get_group (data,num_group=5,factor='momentum'): #输入参数data为包含因子值得原始数据集,num_group为组数,factor为用于排名的因子名称
ranks=data[factor].rank(ascending=True) #按升序排名,组号越大,越好
label=['G'+str(i) for i in range(1,num_group+1) ] #创建组号
category=pd.cut(ranks,bins=num_group,labels=label)
category.name='GROUP'
new_data=data.join(category) #将排名合并入原始数据集中
return new_data
group_datas=datas.groupby(level=0).apply(get_group) #对各时间点进行分组
2.4.6 进行分组回测
1、回测期:2009年1月1日至2018年9月12日;
2、调仓日:按月调仓,每月末按收盘价买卖股票;
3、股票池:中证500成分股;剔除ST股票;剔除由于停牌等原因而无法买卖的股票;
4、回测基准:中证500;
6、单边交易费用:千分之一;
7、分组情况:共分为5组,表现优劣排名为G5>G4>G3>G2>G1;
8、分组回测后得到的是各组与基准的净值,并绘制了净值曲线。
#定义回测函数
def run_by_group(group_data,group_num,list_stock): #输入参数group_data为分组后的数据,group_num组数,list_stock初始的股票池
#构建回测框架
def initialize(context): #定义初始化函数
context.capital = 10000000 #回测的初始资金
context.securities = list_stock #回测标的
context.start_date ='20090123' #回测开始时间
context.end_date = '20180912' #回测结束时间
context.period = 'd' # 'd' 代表日, 'm'代表分钟 表示行情数据的频率
context.benchmark = '000905.SH' #设置回测基准为
context.commission = 0.001 #设置回测的股票交易手续费率为千分之一
context.stock_list=group_data #导入分组后的股票代码
def handle_data(bar_datetime, context, bar_data):
pass
def my_schedule1(bar_datetime, context, bar_data):
bar_datetime_str = bar_datetime.strftime('%Y-%m-%d') #提取回测时间点
code_list = list(context.stock_list.loc[bar_datetime_str].index) #提取该回测时间点上选出的股票代码
wa.change_securities(code_list)
context.securities = code_list #将选取的股票作为新的股票池
list_sell = list(wa.query_position().get_field('code')) #获取持仓中的股票
for code in list_sell:
volumn = wa.query_position()[code]['volume'] #找到每个code的持仓量
res = wa.order(code,volumn,'sell',price='close', volume_check=False) # 卖出上一个月初买入的所有的股票,为本月的建仓做准备
def my_schedule2(bar_datetime, context,bar_data):
buy_code_list = list(set(context.securities)-(set(context.securities)-set(list(bar_data.get_field('code'))))) # 在单因子选股的结果中剔除没有行情的股票
for code in buy_code_list:
res = wa.order_percent(code,1/len(buy_code_list),'buy',price='close', volume_check=False) #对最终选择出来的股票等权配置建仓
wa = BackTest(init_func = initialize, handle_data_func=handle_data) #实例化回测对象
wa.schedule(my_schedule1, "m", -1) #月末按收盘价卖出股票
wa.schedule(my_schedule2, "m", -1) #月末按收盘价买入股票
res = wa.run(show_progress=False) #调用run()函数开始回测,show_progress可用于指定是否显示回测净值曲线图
nav_= wa.summary('nav') #获取回测周期内每一天的组合净值
nav_dic=[di['nav'] for di in nav_] #提取组合的净值
nav_benchmark=[di['benchmark'] for di in nav_] #单独提取基准净值
time=[di['time'] for di in nav_] #提取回测时间
return nav_dic,nav_benchmark,time
#定义分组回测函数
def test_by_group(group_datas,num_group=5): #输入参数group_datas为分完组后的数据,参数num_group为组数
labels=['G'+str(i) for i in range(1,num_group+1) ] #利用num_group(组数)来创建组号
list_stock =w.wset("sectorconstituent","date=20180912;windcode=000905.SH").Data[1] #提取中证500成分股,作为初始股票池
group_dict=OrderedDict() #创建字典用于存储各组净值数据
for group_num in labels:
group_data=group_datas[group_datas['GROUP'].values==group_num] #分组提取数据
nav_dic,nav_benchmark,time=run_by_group(group_data,group_num,list_stock) #分组进行回测,每组回测返回的是组合净值与基准净值
group_dict[group_num]=nav_dic
group_result=pd.DataFrame(group_dict,index=time) #由于各组对应的回测时间相同,所以就用最后一组的回测时间作为索引值
group_result['benchmark']=nav_benchmark #同理,各组的基准净值也相同,所以合并最后一组的基准净值即可
return group_result
besic_result= test_by_group(group_datas,num_group=5) #进行分组测试
besic_result.tail() #得到的是各组的净值
fig,ax1 = plt.subplots(figsize=(16,8.5))
besic_result.plot(legend=True,ax=ax1)
plt.grid()
从净值曲线来看,G3-G5组的表现要优于中证500的表现,G1-G2组劣于中证500;对于表现较为优异的G3,G4,G5,净值曲线并不是很分散,交叉重叠的较多;G5组在前期表现不佳,但后期表现优异;虽然G5表现不是特别突出,但其收益要比其余两组稳定,这一结论从后面的策略表现分析中也能得到证实。
2.4.7 策略表现分析
通过计算各组别与基准的年化收益率、收益波动率、夏普比率、最大回撤等指标,来对比分析各组的收益表现。
……
各组收益表现与前面的净值曲线结果相对应。无论是从收益角度来看,还是从风险角度来看,G5的表现都是最好的,不仅能取得较高收益,而且风险也最小,收益最稳定,G5的sharpe也是最优的,能在承担风险的同时能带来最高的收益。
三、升级版动量策略
3.1 出处
3.2 动量积累的路径很重要
3.3 高质量动量技巧
1、寻找依靠价格缓慢增长实现的高动量;
2、避免大波动造成的高动量。
3.4 技巧说明
价格稳定缓慢增长带来的高动量在样本外更有可能持续走高,而依靠大波动造成的高动量则在样本外难有作为。这两点其实与行为金融学中的两个认知偏差有关:
偏差一:Limited Attention人们的认知资源是有限的。在任何给定的时刻,我们的大脑都偏好于处理那些最显著、最重要的信息,而忽略那些不显著、经济效应微弱的因素。
Da et al. (2014) 提出了一个假说:一系列频繁但微小的变化对于人的吸引力远不如少数却显著的变化;因此投资者对连续信息造成的股价变化反应不足。
提出信息离散性(information discreteness,ID);并猜想ID低(说明信息连续性强)的动量才是高质量动量,而ID高(信息离散性高)的动量是低质量动量,ID的计算方法如下:
ID=sign(过去一段时间的收益率)×(这段时间内下跌交易日%-这段时间内上涨收益日%)
Da et al.(2014)通过实证说明ID是一个优秀的选股因子(越低好),且它能获得Fama-French三因子无法解释的超额收益。
与传统动量相比,通过ID因子筛选找到的高质量动量能够获得更高的超额收益,且该收益在样本外的持续性更强。超额收益的高持续性有助于降低调仓频率、减少换手率、节约交易成本。
偏差二:高估值极端事件发生的概率某些股票的收益率分布是右偏的,这些股票称为“lottery-like” 股票。
行为金融学中的前景理论指出,人们对于极小概率事件发生的主观感受存在认知偏差,会高估它们发生的概率。
对于小概率事件发生可能性的高估导致投资者会过度追逐具有正偏度分布的股票,造成它们的高动量。
Bali et al. (2011) 使用一个称作MAX的代理指标研究了这个问题。MAX是过去1个月内日收益率的最大值(美股不设涨跌停板限制,因此更能反映人们对lottery-like股票追逐的疯狂程度)。
使用 MAX将股票分成10组,MAX值最高的那一组为lottery-like 股票,而MAX值最低的那一组称为“无聊”股票。数据显示,“无聊”股票能显著跑赢lottery-like股票。
3.5 适合A股的改进指标
1、对于信息离散性,Da et al.(2014)给出的 ID 作用不大。考虑到目标是为了挑选平时投资者关注度低的股票,我们选择60个交易日内日换手率的标准差作为ID的代理指标,其越低越好;
2、选用MAX作为lottery -like股票的代理指标,即过去20个交易日的日收益率最大值,它也是越低越好。
3.6 策略思想
1、回测期:2009年1月1日至2018年9月12日;
2、调仓日:按月调仓,每月末按收盘价买卖股票;
3、股票池:中证500成分股;剔除ST股票;剔除由于停牌等原因而无法买卖的股票;
4、交易模型:采用等权重法进行因子打分,将基础版的动量因子Momentum、改进指标ID、改进指标MAX的排名进行加权求平均,得到的平均值即为综合因子得分SCORE;
每月末更新动量指标并重新对股票排名,新股理想仓位为1%,等权配置;
将中证500成分股按综合因子排名分为5组,综合因子值越大越好,按由小到大排名,组别越大的组合越好,即G5组优于G1组;
单边交易费用为千分之一。
3.7 策略实现
3.7.1 构建改进指标ID、MAX 对于MAX指标(过去20个交易日的日收益率最大值)可以直接从API接口中提取,对于ID指标(60个交易日内日换手率的标准差)需要分别提取各调仓日,各股票的过去60个交易日内的日换手率,并计算其标准差。
##提取数据并计算ID与MAX指标
def get_factors(dates_bef60_,dates): #输入参数dates_bef60_为各调仓日前推60日的交易日期, dates为回测期内各个月末交易日(也就是调仓日)
dict_=OrderedDict() #创建每期存取数据字典
for (date_bef60,date) in zip(dates_bef60_,dates):
stocks=get_stocks(date) #提取各交易日的中证500成分股中满足筛选条件的股票池
data=w.wss(stocks, "tech_revs1mmax","tradeDate="+date,usedf=True)[1] #提取MAX指标
data.columns=['MAX']
data['ID']=(w.wsd(stocks, "turn",date_bef60,date, "PriceAdj=F",usedf=True)[1]/100).std() #提取每月末过去60个交易日内的日换手率,计算ID指标
dict_[date]=data
print(date,'success')
datas=pd.concat(dict_.values(),keys=dict_.keys())
datas.index.names=['date','codes']
return datas
datas_=get_factors(dates_bef60_,dates)
datas_.to_csv('data/improve_momentum_strategy_datas.csv') #将基础版动量策略的原始数据存入数据文件中,便于后期读取
3.7.2 构建综合因子
将前面构建的基础版的动量因子Momentum与现在构建的改进指标ID、MAX合并到一块,采用等权重法对因子进行打分得到综合因子SCORE。
datas_=pd.read_csv('data/improve_momentum_strategy_datas.csv',index_col=[0,1]) #读取升级版策略的数据
datas=pd.read_csv('data/basic_momentum_strategy_datas.csv',index_col=[0,1]) #读取基础版策略的数据
datas_=pd.concat([datas_,datas[['momentum']]],axis=1) #将三个因子合并到一个数据框中,以便进行因子打分
# 采用等权重法将改进的两个指标合成一个综合指标
def get_score(data):
rank_data1=data[['MAX','ID']].rank(ascending=False) #将MAX和ID因子按降序排名,排名越大越好
rank_data2=data[['momentum']].rank(ascending=True) #将动量因子升序排名,排名越大越好
rank_data=pd.concat([rank_data1,rank_data2],axis=1)
data['SCORE']=rank_data.mean(axis=1) #等权重计算因子得分形成综合因子,因子值越大越好
return data
datas_score=datas_.groupby(level=0).apply(get_score) #利用得分函数计算综合因子
datas_score.tail()
3.7.3 按综合因子排名进行分组
将综合因子按升序排名并分组后,组号越大的组合表现越优,即G5>G4>G3>G2>G1。
def get_group (data,num_group=5,factor='SCORE'):
ranks=data[factor].rank(ascending=True) #按升序排名,组号越小越好
label=['G'+str(i) for i in range(1,num_group+1) ] #创建组号
category=pd.cut(ranks,bins=num_group,labels=label)
category.name='GROUP'
new_data=data.join(category) #将排名合并入原始数据集中
return new_data
datas_group=datas_score.groupby(level=0).apply(get_group) #对各时间点进行分组
datas_group.tail()
3.7.4 进行分组回测
1、回测期:2009年1月1日至2018年9月12日;
2、调仓日:按月调仓,每月末按收盘价买卖股票;
3、股票池:中证500成分股;剔除ST股票;剔除由于停牌等原因而无法买卖的股票;
4、回测基准:中证500;
6、单边交易费用:千分之一;
7、分组情况:共分为5组,表现优劣排名为G5>G4>G3>G2>G1;
8、分组回测后得到的是各组与基准的净值,并绘制了净值曲线。
从净值曲线可以看出,升级版策略的表现要比基础版的表现更好,因子的单调性也更好,能将表现好坏的不同组清晰的区分出来。
3.7.5 策略表现分析
通过计算各组别与基准的年化收益率、收益波动率、夏普比率、最大回撤等指标,来对比分析各组的收益表现。
……
各组收益表现与前面的净值曲线结果相对应。无论是从收益角度来看,还是从风险角度来看,G5的表现都是最好的,不仅能取得较高收益,而且风险也最小,收益最稳定,G5的sharpe也是最优的,仍能在承担风险的同时能带来最高的收益。
四、基础版策略与升级版策略对比分析
4.1 策略收益对比分析
通过将两个策略的回测净值放于同一副图中,发现基础版策略的表现优于基准的表现,而升级版策略的表现优于基础版策略的表现。
无论是从收益角度来看,还是从风险角度来看,升级版策略的表现都是最好的,不仅能取得较高收益,而且风险也最小,收益最稳定,升级版的sharpe也是最优的,仍能在承担风险的同时能带来最高的收益;同理基础版策略要优于基准;说明策略升级的很成功哦! ^_^
五、动量策略并不适合所有人
5.1 无法继续打败基准
在2017年之前,无论是正收益还是负收益,升级版动量策略的表现都是优于基准的;但从2017年开始,再加上2018年,升级版动量策略的表现变得不如基准的表现好,升级版动量策略无法继续打败基准
5.2 动量策略背后的业务支撑
1、普通交易者的认知偏差造成了市场的定价错误;
2、高昂的套利成本使得聪明投资者无法充分消除定价错误。
5.3 错误定价成因(Barberis et al. 1998 模型)
1、好的盈利消息连续出现会引起投资者的过度反应。投资者产生选择性偏差,过度看中最近发生的这些连续的利好消息、并把这种预期外推到对公司未来股价的预测上;一旦未来的盈利没有达到预期,就会引起他们的恐慌,造成股价的下跌,这是成长投资的特点。
2、当好的盈利消息非连续出现时,投资者会对它们反应不足。这时投资者会出现保守主义偏差。他们会对这个利好持怀疑态度、不情愿更新他们对于该公司基本面的认知,导致无法有效的对股价做出调整。随着时间的推移,当该公司又逐渐出现新的盈利利好时,其股价才会慢慢对其新的基本面反映到位,这就是动量投资。
5.4 有限套利
高昂的套利成本使得聪明投资者无法充分消除定价错误。
有限套利(Limits to Arbitrage): 假设A和B两地都卖苹果,A地价格5块一斤,B地价格10块一斤;苹果B地存在错误定价。面对这个情况,理论上的做法是从A地买苹果然后拿到B地去卖,赚取 5 块钱一斤的差价。这么做的人多了,就会造成B地价格下降,最终消除定价错误。
在现实中,套利者必须考虑各种成本,比如 A 和 B 两地的运输成本,对苹果的储藏成本,两地出租商铺的不同开销以及税收区别;此外,还要有苹果价格下跌的风险 —— 在把苹果从 A 运到 B 的过程中,苹果的价格可能下跌,甚至跌破套利者的成本。这些成本使得聪明人不敢毫无顾虑的套利。
5.5 聪明投资者的顾虑
1、定价错误在短期内不一定会被修正,可能还会进一步放大。
2、对于聪明交易者来说,交易动量策略最大的套利风险是由于动量策略在短期相对于基准指数的弱势表现而造成的职业风险。
3、市场上的资金大多都是short-sighted performance chasers,仅根据短期相对于基准的表现来评估基金经理的业绩。这让基金经理于进退两难的局面。一方面,基金经理希望利用定价错误交易动量策略,因为长期来看这么做的期望收益能够战胜基准;但另一方面,他们这样做的前提是短期内不会威胁到他们手中的资金 —— 投资者不会因为业绩短期跑输基准而赎回资金。
5.6 对动量策略的正确认识
1、动量策略在短期可能会跑输市场,因此并不适合所有人(资金);
2、长期来看,动量策略会战胜市场。对于那些过程驱动、以长期盈利为目标(而忽视短期波动)、能够严格遵守交易纪律的投资者来说,动量策略值得配置;
3、大量跨市场实证指出,动量和价值是两个长期来看负相关的因子。因此,动量策略的最好归宿是和价值策略配合在一起,提高投资组合的风险收益特征。
六、总结
1、两处改进让我们获得了高质量动量因子;提升策略的收益风险比。
2、以上实证仅是为了给大家介绍改进的思路。具体是否使用上述代理指标及参数需要结合不同的动量选股策略来考量。
3、路径很重要!
本课程由WindQuant推出,著作权归作者所有。
资料来源:登录www.windquant.com