量价因子中性化对于因子效果的影响整理转分享


鉴于之前对于非流动性因子做中性化处理之后,得到了更加具有参考意义的结果,本文对量价因子 – 结合价格和成交量构建选股策略中的量价因子进行中性化处理。

  • 中性化之后,量价因子进行六年回测,年化收益率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
站内所有文章皆来自网络转载或读者投稿,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。敬请谅解!

(0)
股市刺客的头像股市刺客
上一篇 35分钟前
下一篇 29分钟前

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注