鉴于之前对于非流动性因子做中性化处理之后,得到了更加具有参考意义的结果,本文对量价因子 – 结合价格和成交量构建选股策略中的量价因子进行中性化处理。
- 中性化之后,量价因子进行六年回测,年化收益率23.9%,阿尔法19.0%,贝塔0.95,夏普比率0.71,收益波动率28.5%,信息比率达到2.71
2. 量价因子构建
量价因子构建的部分参见量价因子 – 结合价格和成交量构建选股策略,此处不再赘述。
import matplotlib.pyplot as plt
from matplotlib import rc
from matplotlib import dates
rc('mathtext', default='regular')
import seaborn as sns
sns.set_style('white')
import datetime
import numpy as np
import pandas as pd
import time
import scipy.stats as st
from CAL.PyCAL import * # CAL.PyCAL中包含font
3. 量价因子截面特征
3.1 首先加载计算好的数据文件:
# 提取数据
corr_data = pd.read_csv('VolPriceCorr_W15_FullA.csv') # 15天窗口量价相关系数
forward_20d_return_data = pd.read_csv('ForwardReturns_W20_FullA.csv') # 未来20天收益率
backward_20d_return_data = pd.read_csv('BackwardReturns_W20_FullA.csv') # 过去20天收益率
backward_60d_return_data = pd.read_csv('BackwardReturns_W60_FullA.csv') # 过去60天收益率
mkt_value_data = pd.read_csv('MarketValues_FullA.csv') # 市值数据
turnover_rate_data = pd.read_csv('TurnoverRateWindowMean_W20_FullA.csv') # 过去20天日均换手率数据
corr_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, corr_data['tradeDate']))
forward_20d_return_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, forward_20d_return_data['tradeDate']))
backward_20d_return_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, backward_20d_return_data['tradeDate']))
backward_60d_return_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, backward_60d_return_data['tradeDate']))
mkt_value_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, mkt_value_data['tradeDate']))
turnover_rate_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, turnover_rate_data['tradeDate']))
corr_data = corr_data[corr_data.columns[1:]].set_index('tradeDate')
forward_20d_return_data = forward_20d_return_data[forward_20d_return_data.columns[1:]].set_index('tradeDate')
backward_20d_return_data = backward_20d_return_data[backward_20d_return_data.columns[1:]].set_index('tradeDate')
backward_60d_return_data = backward_60d_return_data[backward_60d_return_data.columns[1:]].set_index('tradeDate')
mkt_value_data = mkt_value_data[mkt_value_data.columns[1:]].set_index('tradeDate')
turnover_rate_data = turnover_rate_data[turnover_rate_data.columns[1:]].set_index('tradeDate')
corr_data.tail()
上表中,展示了我们计算好的corr_data数据文件的一部分,主要为了说明我们接下来使用的数据dataframe的结构:
- 每一行为日期,每个交易日均有计算数据,从2006年到2016年8月
- 每一列为股票,股票池为全A股
3.2 量价因子中性化
- 中性化使用Uqer的中性化函数 neutralize (自从发现这个好东西,简直欲罢不能)
- 简单地说,就是对量价因子进行截面回归,将其中的风格因子、行业因子等风险因子从量价因子中剔除
# 量价因子进行中性化
# 中性化使用Uqer的中性化函数 neutralize
corr_neutral_data = corr_data.copy(deep=True)
for dt in corr_data.index:
dt_str = dt.strftime('%Y%m%d')
try:
corr_neutral_data.ix[dt] = pd.Series(neutralize(corr_data.ix[dt].to_dict(), target_date=dt_str))
except:
print dt_str
continue
corr_neutral_data.to_csv('VolPriceCorr_Neutral_W15_FullA.csv') # 中性化之后的数据保存起来
# 提取数据
corr_neutral_data = pd.read_csv('VolPriceCorr_Neutral_W15_FullA.csv') # 量价因子进行中性化
corr_neutral_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, corr_neutral_data['tradeDate']))
corr_neutral_data = corr_neutral_data.set_index('tradeDate')
# 刚刚开始几日量价因子无数据,需要截掉
corr_data = corr_data.ix[15:]
corr_neutral_data = corr_neutral_data.ix[15:]
corr_neutral_data.tail()
3.3 量价相关因子截面特征
接下来,我们简单检查一下我们计算得到的量价相关因子的截面特征,分别对中性化前、中性化后的因子值检查截面特征
# 原始的未作中性化的量价相关性因子历史表现
n_quantile = 10
# 统计十分位数
cols_mean = ['meanQ'+str(i+1) for i in range(n_quantile)]
cols = cols_mean
corr_means = pd.DataFrame(index=corr_data.index, columns=cols)
# 计算相关系数分组平均值
for dt in corr_means.index:
qt_mean_results = []
# 相关系数去掉nan和绝对值大于1的
tmp_corr = corr_data.ix[dt].dropna()
tmp_corr = tmp_corr[(tmp_corr<=1.0) & (tmp_corr>=-1.0)]
pct_quantiles = 1.0/n_quantile
for i in range(n_quantile):
down = tmp_corr.quantile(pct_quantiles*i)
up = tmp_corr.quantile(pct_quantiles*(i+1))
mean_tmp = tmp_corr[(tmp_corr<=up) & (tmp_corr>=down)].mean()
qt_mean_results.append(mean_tmp)
corr_means.ix[dt] = qt_mean_results
# corr_means是对历史每一天,求量价相关系数在各个十分位里面的平均值
# ------------- 原始的未作中性化的量价相关性因子历史表现作图 ------------------------
fig = plt.figure(figsize=(16, 6))
ax1 = fig.add_subplot(111)
lns1 = ax1.plot(corr_means.index, corr_means.meanQ1, label='Q1')
lns2 = ax1.plot(corr_means.index, corr_means.meanQ5, label='Q5')
lns3 = ax1.plot(corr_means.index, corr_means.meanQ10, label='Q10')
lns = lns1+lns2+lns3
labs = [l.get_label() for l in lns]
ax1.legend(lns, labs, bbox_to_anchor=[0.5, 0.1], loc='', ncol=3, mode="", borderaxespad=0., fontsize=12)
ax1.set_ylabel(u'量价相关因子', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'日期', fontproperties=font, fontsize=16)
ax1.set_title(u"原始的未作中性化的量价相关性因子历史表现", fontproperties=font, fontsize=16)
ax1.grid()

# 中性化之后的量价相关性因子历史表现n_quantile = 10# 统计十分位数cols_mean = ['meanQ'+str(i+1) for i in range(n_quantile)]cols = cols_meancorr_means = pd.DataFrame(index=corr_neutral_data.index, columns=cols)# 计算相关系数分组平均值for dt in corr_means.index: qt_mean_results = [] # 相关系数去掉nan和绝对值大于1的 tmp_corr = corr_neutral_data.ix[dt].dropna() tmp_corr = tmp_corr[(tmp_corr<=1.0) & (tmp_corr>=-1.0)] pct_quantiles = 1.0/n_quantile for i in range(n_quantile): down = tmp_corr.quantile(pct_quantiles*i) up = tmp_corr.quantile(pct_quantiles*(i+1)) mean_tmp = tmp_corr[(tmp_corr<=up) & (tmp_corr>=down)].mean() qt_mean_results.append(mean_tmp) corr_means.ix[dt] = qt_mean_results# corr_means是对历史每一天,求量价相关系数在各个十分位里面的平均值# ------------- 中性化之后的量价相关性因子历史表现作图 ------------------------fig = plt.figure(figsize=(16, 6))ax1 = fig.add_subplot(111)lns1 = ax1.plot(corr_means.index, corr_means.meanQ1, label='Q1')lns2 = ax1.plot(corr_means.index, corr_means.meanQ5, label='Q5')lns3 = ax1.plot(corr_means.index, corr_means.meanQ10, label='Q10')lns = lns1+lns2+lns3labs = [l.get_label() for l in lns]ax1.legend(lns, labs, bbox_to_anchor=[0.5, 0.1], loc='', ncol=3, mode="", borderaxespad=0., fontsize=12)ax1.set_ylabel(u'量价相关因子', fontproperties=font, fontsize=16)ax1.set_xlabel(u'日期', fontproperties=font, fontsize=16)ax1.set_title(u"中性化之后的量价相关性历史表现", fontproperties=font, fontsize=16)ax1.grid()

上面两张图(分别对应原始量价因子、中性化之后的量价因子)给出了2006年至2016年间,在不同时点,将市场上所有股票按量价相关性分10组后,第1组、第5组以及第10组股票量价相关性的均值情况,即我们所说的量价相关性截面特征:
- 观察可知,量价相关性的截面特征较为稳定
- 相对来说,中性化之后的因子截面特征更为稳定
3.4 量价因子的预测能力初探
接下来,我们计算了每一天的量价因子和之后20日收益的秩相关系数,分别考虑中性化之前和之后的情况
# ‘过去十五天量价相关系数’和‘之后20天收益’的秩相关系数计算
ic_data = pd.DataFrame(index=corr_data.index, columns=['IC','pValue'])
# 计算相关系数
for dt in ic_data.index:
tmp_corr = corr_data.ix[dt]
tmp_ret = forward_20d_return_data.ix[dt]
cor = pd.DataFrame(tmp_corr)
ret = pd.DataFrame(tmp_ret)
cor.columns = ['corr']
ret.columns = ['ret']
cor['ret'] = ret['ret']
cor = cor[~np.isnan(cor['corr'])][~np.isnan(cor['ret'])]
if len(cor) < 5:
continue
# ic,p_value = st.pearsonr(q['Q'],q['ret']) # 计算相关系数 IC
# ic,p_value = st.pearsonr(q['Q'].rank(),q['ret'].rank()) # 计算秩相关系数 RankIC
ic, p_value = st.spearmanr(cor['corr'],cor['ret']) # 计算秩相关系数 RankIC
ic_data['IC'][dt] = ic
ic_data['pValue'][dt] = p_value
# print len(ic_data['IC']), len(ic_data[ic_data.IC>0]), len(ic_data[ic_data.IC<0])
print 'mean of IC: ', ic_data['IC'].mean()
print 'median of IC: ', ic_data['IC'].median()
print 'the number of IC(all, plus, minus): ', (len(ic_data), len(ic_data[ic_data.IC>0]), len(ic_data[ic_data.IC<0]))
# ‘过去十五天量价相关系数’和‘之后20天收益’的秩相关系数作图
fig = plt.figure(figsize=(16, 6))
ax1 = fig.add_subplot(111)
lns1 = ax1.plot(ic_data[ic_data>0].index, ic_data[ic_data>0].IC, '.r', label='IC(plus)')
lns2 = ax1.plot(ic_data[ic_data<0].index, ic_data[ic_data<0].IC, '.b', label='IC(minus)')
lns = lns1+lns2
labs = [l.get_label() for l in lns]
ax1.legend(lns, labs, bbox_to_anchor=[0.6, 0.1], loc='', ncol=2, mode="", borderaxespad=0., fontsize=12)
ax1.set_ylabel(u'因子日度IC', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'日期', fontproperties=font, fontsize=16)
ax1.set_title(u"原始的未作中性化的量价因子和之后20日收益的秩相关系数", fontproperties=font, fontsize=16)
ax1.grid()

# ‘过去十五天量价相关系数’和‘之后20天收益’的秩相关系数计算ic_data = pd.DataFrame(index=corr_neutral_data.index, columns=['IC','pValue'])# 计算相关系数for dt in ic_data.index: tmp_corr = corr_neutral_data.ix[dt] tmp_ret = forward_20d_return_data.ix[dt] cor = pd.DataFrame(tmp_corr) ret = pd.DataFrame(tmp_ret) cor.columns = ['corr'] ret.columns = ['ret'] cor['ret'] = ret['ret'] cor = cor[~np.isnan(cor['corr'])][~np.isnan(cor['ret'])] if len(cor) < 5: continue # ic,p_value = st.pearsonr(q['Q'],q['ret']) # 计算相关系数 IC # ic,p_value = st.pearsonr(q['Q'].rank(),q['ret'].rank()) # 计算秩相关系数 RankIC ic, p_value = st.spearmanr(cor['corr'],cor['ret']) # 计算秩相关系数 RankIC ic_data['IC'][dt] = ic ic_data['pValue'][dt] = p_value # print len(ic_data['IC']), len(ic_data[ic_data.IC>0]), len(ic_data[ic_data.IC<0])print 'mean of IC: ', ic_data['IC'].mean()print 'median of IC: ', ic_data['IC'].median()print 'the number of IC(all, plus, minus): ', (len(ic_data), len(ic_data[ic_data.IC>0]), len(ic_data[ic_data.IC<0]))# ‘过去十五天量价相关系数’和‘之后20天收益’的秩相关系数作图fig = plt.figure(figsize=(16, 6))ax1 = fig.add_subplot(111)lns1 = ax1.plot(ic_data[ic_data>0].index, ic_data[ic_data>0].IC, '.r', label='IC(plus)')lns2 = ax1.plot(ic_data[ic_data<0].index, ic_data[ic_data<0].IC, '.b', label='IC(minus)')lns = lns1+lns2labs = [l.get_label() for l in lns]ax1.legend(lns, labs, bbox_to_anchor=[0.6, 0.1], loc='', ncol=2, mode="", borderaxespad=0., fontsize=12)ax1.set_ylabel(u'因子日度IC', fontproperties=font, fontsize=16)ax1.set_xlabel(u'日期', fontproperties=font, fontsize=16)ax1.set_title(u"中性化之后的量价因子和之后20日收益的秩相关系数", fontproperties=font, fontsize=16)ax1.grid()
从上面两图可知:
- 做不做中性化处理,量价因子和未来20日收益的秩相关系数都在大部分时间为负,量价因子对于未来20天的收益都有预测性
- 中性化之后的IC平均值为 -0.047,比中性化之前的IC平均值 -0.041 实际上有所提升
- 从历史上看,中性化之后,更多的因子日度IC为负,预示着中性化之后量价因子表现会更好
4. 量价因子历史回测概述
本节使用2006年以来的数据对于量价相关性因子历史表现进行回测,进一步简单涉及量价因子选股的几个风险因子暴露情况;均分别考虑原始量价因子和中性化之后的量价因子
4.1 量价因子选股的分组超额收益
def quantile_excess_returns(signal_df, return_df):
n_quantile = 10
# 统计十分位数
cols_mean = [i+1 for i in range(n_quantile)]
cols = cols_mean
# 计算中性化前量价因子选股分组的超额收益平均值
excess_returns_means = pd.DataFrame(index=signal_df.index, columns=cols)
for dt in excess_returns_means.index:
qt_mean_results = []
# 相关系数去掉nan和绝对值大于0.97的
tmp_corr = signal_df.ix[dt].dropna()
tmp_corr = tmp_corr[(tmp_corr<=0.97) & (tmp_corr>=-0.97)]
tmp_return = return_df.ix[dt].dropna()
tmp_return_mean = tmp_return.mean()
pct_quantiles = 1.0/n_quantile
for i in range(n_quantile):
down = tmp_corr.quantile(pct_quantiles*i)
up = tmp_corr.quantile(pct_quantiles*(i+1))
i_quantile_index = tmp_corr[(tmp_corr<=up) & (tmp_corr>=down)].index
mean_tmp = tmp_return[i_quantile_index].mean() - tmp_return_mean
qt_mean_results.append(mean_tmp)
excess_returns_means.ix[dt] = qt_mean_results
excess_returns_means.dropna(inplace=True)
return excess_returns_means.mean()
# 计算中性化前量价因子选股分组的超额收益平均值
excess_returns_means = quantile_excess_returns(corr_data, forward_20d_return_data)
neutral_returns_means = quantile_excess_returns(corr_neutral_data, forward_20d_return_data)
# 计算量价因子选股分组的超额收益平均值作图
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(111)
width = 0.3
lns1 = ax1.bar(excess_returns_means.index-width/2, excess_returns_means.values, align='center', color='g', width=width)
lns2 = ax1.bar(neutral_returns_means.index+width/2, neutral_returns_means.values, align='center', color='r', width=width)
ax1.legend(['origin signal','neutralized signal'], fontsize=14)
ax1.set_xlim(left=0.5, right=len(excess_returns_means_dist)+0.5)
ax1.set_ylabel(u'超额收益', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'十分位分组', fontproperties=font, fontsize=16)
ax1.set_xticks(excess_returns_means_dist.index)
ax1.set_xticklabels([int(x) for x in ax1.get_xticks()], fontproperties=font, fontsize=14)
ax1.set_yticklabels([str(x*100)+'0%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)
ax1.set_title(u"量价因子分组超额收益", fontproperties=font, fontsize=16)
ax1.grid()

- 图中展示,原始量价因子和中性化之后的量价因子,十分位选股后,在未来一个月各个分组的超额收益,绿柱为原始量价因子,红柱为中性化后的量价因子
- 中性化之后的量价因子,多头收益有所增加,空头收益有所减小
4.2 量价因子选股的市值分布特征
检查量价因子的小市值暴露情况。因为很多策略因为小市值暴露在A股市场表现优异。
# 计算量价因子分组的市值分位数平均值
def quantile_mkt_values(signal_df, mkt_df):
n_quantile = 10
# 统计十分位数
cols_mean = [i+1 for i in range(n_quantile)]
cols = cols_mean
mkt_value_means = pd.DataFrame(index=signal_df.index, columns=cols)
# 计算相关系数分组的市值分位数平均值
for dt in mkt_value_means.index:
qt_mean_results = []
# 相关系数去掉nan和绝对值大于0.97的
tmp_corr = signal_df.ix[dt].dropna()
tmp_corr = tmp_corr[(tmp_corr<=0.97) & (tmp_corr>=-0.97)]
tmp_mkt_value = mkt_df.ix[dt].dropna()
tmp_mkt_value = tmp_mkt_value.rank()/len(tmp_mkt_value)
pct_quantiles = 1.0/n_quantile
for i in range(n_quantile):
down = tmp_corr.quantile(pct_quantiles*i)
up = tmp_corr.quantile(pct_quantiles*(i+1))
i_quantile_index = tmp_corr[(tmp_corr<=up) & (tmp_corr>=down)].index
mean_tmp = tmp_mkt_value[i_quantile_index].mean()
qt_mean_results.append(mean_tmp)
mkt_value_means.ix[dt] = qt_mean_results
mkt_value_means.dropna(inplace=True)
return mkt_value_means.mean()
# 计算量价因子分组的市值分位数平均值
origin_mkt_means = quantile_mkt_values(corr_data, mkt_value_data)
neutral_mkt_means = quantile_mkt_values(corr_neutral_data, mkt_value_data)
# 量价因子分组的市值分位数平均值作图
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(111)
width = 0.3
lns1 = ax1.bar(origin_mkt_means.index-width/2, origin_mkt_means.values, align='center', color='g', width=width)
lns2 = ax1.bar(neutral_mkt_means.index+width/2, neutral_mkt_means.values, align='center', color='r', width=width)
ax1.set_ylim(0.4,0.6)
ax1.legend(['origin signal','neutralized signal'], fontsize=14)
ax1.set_xlim(left=0.5, right=len(origin_mkt_means)+0.5)
ax1.set_ylabel(u'市值百分位数', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'十分位分组', fontproperties=font, fontsize=16)
ax1.set_xticks(origin_mkt_means.index)
ax1.set_xticklabels([int(x) for x in ax1.get_xticks()], fontproperties=font, fontsize=14)
ax1.set_yticklabels([str(x*100)+'0%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)
ax1.set_title(u"量价因子分组市值分布特征", fontproperties=font, fontsize=16)
ax1.grid()

- 上图展示,原始量价因子多头有略微的大市值暴露,中性化则可以去除多头的大市值暴露
- 鉴于小盘股有更好的成长性,我们期待中性化之后的量价因子能够带来更好的收益
5. 量价因子历史回测净值表现
接下来,考察原始量价因子和中性化后的量价因子的选股能力的回测效果。历史回测的基本设置如下:
- 回测时段为2010年1月1日至2016年8月1日
- 股票池为A股全部股票
- 组合每15个交易日调仓,交易费率设为双边万分之二
- 调仓时,涨停、停牌不买入,跌停、停牌不卖出;
- 每次调仓时,选择股票池中量价因子最小的20%的股票;
5.1 原始量价因子
bt_all = {} # 用来保存两个策略运行结果:原始量价因子,中性化后量价因子
start = '2010-01-01' # 回测起始时间
end = '2016-08-01' # 回测结束时间
benchmark = 'ZZ500' # 策略参考标准
universe = set_universe('A') # 证券池,支持股票和基金
capital_base = 10000000 # 起始资金
freq = 'd' # 策略类型,'d'表示日间策略使用日线回测
refresh_rate = 15 # 调仓频率,表示执行handle_data的时间间隔
corr_data = pd.read_csv('VolPriceCorr_W15_FullA.csv') # 读取量价因子数据
corr_data = corr_data[corr_data.columns[1:]].set_index('tradeDate')
corr_dates = corr_data.index.values
quantile_five = 1 # 选取股票的量价因子五分位数,1表示选取股票池中因子最小的10%的股票
commission = Commission(0.0002,0.0002) # 交易费率设为双边万分之二
def initialize(account): # 初始化虚拟账户状态
pass
def handle_data(account): # 每个交易日的买入卖出指令
pre_date = account.previous_date.strftime("%Y-%m-%d")
if pre_date not in corr_dates: # 只在计算过量价因子的交易日调仓
return
# 拿取调仓日前一个交易日的量价因子,并按照相应十分位选择股票
pre_corr = corr_data.ix[pre_date]
pre_corr = pre_corr.dropna()
pre_corr = pre_corr[(pre_corr<=1.0) & (pre_corr>=-1.0)]
pre_corr_min = pre_corr.quantile((quantile_five-1)*0.2)
pre_corr_max = pre_corr.quantile(quantile_five*0.2)
my_univ = pre_corr[pre_corr>=pre_corr_min][pre_corr<pre_corr_max].index.values
# 调仓逻辑
univ = [x for x in my_univ if x in account.universe]
# 不在股票池中的,清仓
for stk in account.valid_secpos:
if stk not in univ:
order_to(stk, 0)
# 在目标股票池中的,等权买入
for stk in univ:
order_pct_to(stk, 1.1/len(univ))

bt_all['origin'] = bt
5.2 中性化之后的量价因子
start = '2010-01-01' # 回测起始时间end = '2016-08-01' # 回测结束时间benchmark = 'ZZ500' # 策略参考标准universe = set_universe('A') # 证券池,支持股票和基金capital_base = 10000000 # 起始资金freq = 'd' # 策略类型,'d'表示日间策略使用日线回测refresh_rate = 15 # 调仓频率,表示执行handle_data的时间间隔corr_data = pd.read_csv('VolPriceCorr_Neutral_W15_FullA.csv') # 读取量价因子数据corr_data = corr_data[corr_data.columns[:]].set_index('tradeDate')corr_dates = corr_data.index.valuesquantile_five = 1 # 选取股票的量价因子五分位数,1表示选取股票池中因子最小的10%的股票commission = Commission(0.0002,0.0002) # 交易费率设为双边万分之二def initialize(account): # 初始化虚拟账户状态 passdef handle_data(account): # 每个交易日的买入卖出指令 pre_date = account.previous_date.strftime("%Y-%m-%d") if pre_date not in corr_dates: # 只在计算过量价因子的交易日调仓 return # 拿取调仓日前一个交易日的量价因子,并按照相应十分位选择股票 pre_corr = corr_data.ix[pre_date] pre_corr = pre_corr.dropna() pre_corr = pre_corr[(pre_corr<=1.0) & (pre_corr>=-1.0)] pre_corr_min = pre_corr.quantile((quantile_five-1)*0.2) pre_corr_max = pre_corr.quantile(quantile_five*0.2) my_univ = pre_corr[pre_corr>=pre_corr_min][pre_corr<pre_corr_max].index.values # 调仓逻辑 univ = [x for x in my_univ if x in account.universe] # 不在股票池中的,清仓 for stk in account.valid_secpos: if stk not in univ: order_to(stk, 0) # 在目标股票池中的,等权买入 for stk in univ: order_pct_to(stk, 1.1/len(univ))

bt_all['neutralized'] = bt
5.3 中性化前后的量价因子回测对比
results = {}for x in bt_all.keys(): results[x] = {} results[x]['bt'] = bt_all[x]
fig = plt.figure(figsize=(10,8))fig.set_tight_layout(True)ax1 = fig.add_subplot(211)ax2 = fig.add_subplot(212)ax1.grid()ax2.grid()for qt in ['origin','neutralized']: bt = results[qt]['bt'] data = bt[[u'tradeDate',u'portfolio_value',u'benchmark_return']] data['portfolio_return'] = data.portfolio_value/data.portfolio_value.shift(1) - 1.0 # 总头寸每日回报率 data['portfolio_return'].ix[0] = data['portfolio_value'].ix[0]/ 10000000.0 - 1.0 data['excess_return'] = data.portfolio_return - data.benchmark_return # 总头寸每日超额回报率 data['excess'] = data.excess_return + 1.0 data['excess'] = data.excess.cumprod() # 总头寸对冲指数后的净值序列 data['portfolio'] = data.portfolio_return + 1.0 data['portfolio'] = data.portfolio.cumprod() # 总头寸不对冲时的净值序列 data['benchmark'] = data.benchmark_return + 1.0 data['benchmark'] = data.benchmark.cumprod() # benchmark的净值序列 results[qt]['hedged_max_drawdown'] = max([1 - v/max(1, max(data['excess'][:i+1])) for i,v in enumerate(data['excess'])]) # 对冲后净值最大回撤 results[qt]['hedged_volatility'] = np.std(data['excess_return'])*np.sqrt(252) results[qt]['hedged_annualized_return'] = (data['excess'].values[-1])**(252.0/len(data['excess'])) - 1.0 # data[['portfolio','benchmark','excess']].plot(figsize=(12,8)) # ax.plot(data[['portfolio','benchmark','excess']], label=str(qt)) ax1.plot(data['tradeDate'], data[['portfolio']], label=str(qt)) ax2.plot(data['tradeDate'], data[['excess']], label=str(qt)) ax1.legend(loc=0, fontsize=12)ax2.legend(loc=0, fontsize=12)ax1.set_ylabel(u"净值", fontproperties=font, fontsize=16)ax2.set_ylabel(u"对冲净值", fontproperties=font, fontsize=16)ax1.set_title(u"量价因子中性化前后选股能力对比 - 净值走势", fontproperties=font, fontsize=16)ax2.set_title(u"量价因子中性化前后选股能力对比 - 对冲中证500指数后净值走势", fontproperties=font, fontsize=16)

上图中可以发现:
- 蓝色曲线为原始量价因子,绿色为中性化之后的量价因子
- 中性化之前:年化收益率22.4%,阿尔法17.5%,贝塔0.92,夏普比率0.67,收益波动率27.9%,信息比率2.22
- 中性化之后:年化收益率23.9%,阿尔法19.0%,贝塔0.95,夏普比率0.71,收益波动率28.5%,信息比率2.71
- 可以看到,中性化对于量价因子的择股能力并无削弱,反映出量价因子本身和各个风格因子的相关度比较低
5.5 中性化之后量价因子选股 —— 不同五分位数组合回测走势比较
# 可编辑部分与 strategy 模式一样,其余部分按本例代码编写即可
# -----------回测参数部分开始,可编辑------------
start = '2010-01-01' # 回测起始时间
end = '2016-08-01' # 回测结束时间
benchmark = 'ZZ500' # 策略参考标准
universe = set_universe('A') # 证券池,支持股票和基金
capital_base = 10000000 # 起始资金
freq = 'd' # 策略类型,'d'表示日间策略使用日线回测
refresh_rate = 15 # 调仓频率,表示执行handle_data的时间间隔
corr_data = pd.read_csv('VolPriceCorr_Neutral_W15_FullA.csv') # 读取量价因子数据
corr_data = corr_data[corr_data.columns[:]].set_index('tradeDate')
corr_dates = corr_data.index.values
# ---------------回测参数部分结束----------------
# 把回测参数封装到 SimulationParameters 中,供 quick_backtest 使用
sim_params = quartz.SimulationParameters(start, end, benchmark, universe, capital_base)
# 获取回测行情数据
idxmap, data = quartz.get_daily_data(sim_params)
# 运行结果
results_corr = {}
# 调整参数(选取股票的量价因子五分位数),进行快速回测
for quantile_five in range(1, 6):
# ---------------策略逻辑部分----------------
commission = Commission(0.0002,0.0002) # 交易费率设为双边万分之二
def initialize(account): # 初始化虚拟账户状态
pass
def handle_data(account): # 每个交易日的买入卖出指令
pre_date = account.previous_date.strftime("%Y-%m-%d")
if pre_date not in corr_dates: # 只在计算过量价因子的交易日调仓
return
# 拿取调仓日前一个交易日的量价因子,并按照相应十分位选择股票
pre_corr = corr_data.ix[pre_date]
pre_corr = pre_corr.dropna()
pre_corr = pre_corr[(pre_corr<=1.0) & (pre_corr>=-1.0)]
pre_corr_min = pre_corr.quantile((quantile_five-1)*0.2)
pre_corr_max = pre_corr.quantile(quantile_five*0.2)
my_univ = pre_corr[pre_corr>=pre_corr_min][pre_corr<pre_corr_max].index.values
# 调仓逻辑
univ = [x for x in my_univ if x in account.universe]
# 不在股票池中的,清仓
for stk in account.valid_secpos:
if stk not in univ:
order_to(stk, 0)
# 在目标股票池中的,等权买入
for stk in univ:
order_pct_to(stk, 1.1/len(univ))
# ---------------策略逻辑部分结束----------------
# 把回测逻辑封装到 TradingStrategy 中,供 quick_backtest 使用
strategy = quartz.TradingStrategy(initialize, handle_data)
# 回测部分
bt, acct = quartz.quick_backtest(sim_params, strategy, idxmap, data, refresh_rate=refresh_rate, commission=commission)
# 对于回测的结果,可以通过 perf_parse 函数计算风险指标
perf = quartz.perf_parse(bt, acct)
# 保存运行结果
tmp = {}
tmp['bt'] = bt
tmp['annualized_return'] = perf['annualized_return']
tmp['volatility'] = perf['volatility']
tmp['max_drawdown'] = perf['max_drawdown']
tmp['alpha'] = perf['alpha']
tmp['beta'] = perf['beta']
tmp['sharpe'] = perf['sharpe']
tmp['information_ratio'] = perf['information_ratio']
results_corr[quantile_five] = tmp
print str(quantile_five),
print 'done'
1
2
3
4
5 done
fig = plt.figure(figsize=(10,8))
fig.set_tight_layout(True)
ax1 = fig.add_subplot(211)
ax2 = fig.add_subplot(212)
ax1.grid()
ax2.grid()
for qt in results_corr:
bt = results_corr[qt]['bt']
data = bt[[u'tradeDate',u'portfolio_value',u'benchmark_return']]
data['portfolio_return'] = data.portfolio_value/data.portfolio_value.shift(1) - 1.0 # 总头寸每日回报率
data['portfolio_return'].ix[0] = data['portfolio_value'].ix[0]/ 10000000.0 - 1.0
data['excess_return'] = data.portfolio_return - data.benchmark_return # 总头寸每日超额回报率
data['excess'] = data.excess_return + 1.0
data['excess'] = data.excess.cumprod() # 总头寸对冲指数后的净值序列
data['portfolio'] = data.portfolio_return + 1.0
data['portfolio'] = data.portfolio.cumprod() # 总头寸不对冲时的净值序列
data['benchmark'] = data.benchmark_return + 1.0
data['benchmark'] = data.benchmark.cumprod() # benchmark的净值序列
results_corr[qt]['hedged_max_drawdown'] = max([1 - v/max(1, max(data['excess'][:i+1])) for i,v in enumerate(data['excess'])]) # 对冲后净值最大回撤
results_corr[qt]['hedged_volatility'] = np.std(data['excess_return'])*np.sqrt(252)
results_corr[qt]['hedged_annualized_return'] = (data['excess'].values[-1])**(252.0/len(data['excess'])) - 1.0
# data[['portfolio','benchmark','excess']].plot(figsize=(12,8))
# ax.plot(data[['portfolio','benchmark','excess']], label=str(qt))
ax1.plot(data['tradeDate'], data[['portfolio']], label=str(qt))
ax2.plot(data['tradeDate'], data[['excess']], label=str(qt))
ax1.legend(loc=0, fontsize=12)
ax2.legend(loc=0, fontsize=12)
ax1.set_ylabel(u"净值", fontproperties=font, fontsize=16)
ax2.set_ylabel(u"对冲净值", fontproperties=font, fontsize=16)
ax1.set_title(u"量价因子 - 不同五分位数分组选股净值走势", fontproperties=font, fontsize=16)
ax2.set_title(u"量价因子 - 不同五分位数分组选股对冲中证500指数后净值走势", fontproperties=font, fontsize=16)
# results 转换为 DataFrame
results_pd = pd.DataFrame(results_corr).T.sort_index()
results_pd = results_pd[[u'alpha', u'beta', u'information_ratio', u'sharpe',
u'annualized_return', u'max_drawdown', u'volatility',
u'hedged_annualized_return', u'hedged_max_drawdown', u'hedged_volatility']]
for col in results_pd.columns:
results_pd[col] = [np.round(x, 3) for x in results_pd[col]]
cols = [(u'风险指标', u'Alpha'), (u'风险指标', u'Beta'), (u'风险指标', u'信息比率'), (u'风险指标', u'夏普比率'),
(u'纯股票多头时', u'年化收益'), (u'纯股票多头时', u'最大回撤'), (u'纯股票多头时', u'收益波动率'),
(u'对冲后', u'年化收益'), (u'对冲后', u'最大回撤'),
(u'对冲后', u'收益波动率')]
results_pd.columns = pd.MultiIndex.from_tuples(cols)
results_pd.index.name = u'五分位组别'
results_pd


上面的图片显示“量价因子(中性化之后)-不同五分位数分组选股”的净值走势,其中下面一张图片展示出各组头寸对冲完中证500指数后的净值走势,可以看到:
- 不同的五分位数组对应的净值走势顺序区分度比较高!
- 如果仔细留心之前原始量价因子的五分位数回测的话,会发现中性化会使得量价因子的五分位走势区分度变高许多
总结
- 很多选股因子,包括之前大家看到的聪明钱和非流动性,都具有或多或少的小市值暴露,等你中性化完了,超额收益就少一点;但是此处的量价因子,却是大市值暴露,自然要特别考虑到大蓝筹死水一潭的极端情况。
发布者:股市刺客,转载请注明出处:https://www.95sca.cn/archives/321274
站内所有文章皆来自网络转载或读者投稿,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。敬请谅解!