参考了东方证券的研报,对上下波动率因子进行了测试。
- 东方证券:《因子选股系列研究之二十》:技术类新Alpha因子的批量测试
- Chen, J., Hong, H., and Stein, J.C. (2001). Forecasting crashes: Trading volume, past returns, and conditional skewness in stock prices. Journal of financial Economics, vol.61(3), pp.345-381.
1.1 因子的定义
上下行波动率是历史收益率低于平均收益率的下行波动率比上历史收益率高平均收益率的上行波动率的比率:

其中,nu为大于平均复合收益率的天数,nd为小于平均复合收益率的天数, rit为股票的日收益率,rl¯为股票的60日平均收益率。这是一个衡量股价暴跌可能性的指标,学界通常认为DUVOL较高的股票有着更高的暴跌可能,因此也就有着期望更高的风险溢价。 需要注意的几点:
- 为了计算方便,本文使用的是有偏的日波动率
- 本文的价格即当日真实的收盘价格
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
from matplotlib import rc
rc('mathtext', default='regular')
import seaborn as sns
sns.set_style('white')
from matplotlib import dates
import numpy as np
import pandas as pd
import statsmodels.api as sm
import time
import scipy.stats as st
from CAL.PyCAL import * # CAL.PyCAL中包含font
universe=set_universe('A')
1.2 因子的计算
可以通过下面的函数来获取数据
def getUqerStockFactors(begin, end, factor, universe=None):
"""
使用优矿的因子DataAPI,拿取所需的因子,并整理成相应格式,返回的因子数据
格式为pandas MultiIndex Series,例如:
secID tradeDate
002130.XSHE 2016-12-20 0.0640
2016-12-21 0.0643
2016-12-22 0.0631
2016-12-23 0.0641
2016-12-26 0.0667
注意:后面拿取因子的DataAPI可以自行切换为专业版API,以获取更多因子支持
Parameters
----------
begin : str
开始日期,'YYYY-mm-dd' 或 'YYYYmmdd'
end : str
截止日期,'YYYY-mm-dd' 或 'YYYYmmdd'
universe: list
股票池,格式为uqer股票代码secID,若为None则默认拿取全部A股
factor: str
因子名称,uqer的DataAPI.MktStockFactorsOneDayGet(或
专业版对应API)可查询的因子
Returns
-------
df : pd.DataFrame
因子数据,index为tradeDate,columns为secID
"""
# 拿取上海证券交易所日历
cal_dates = DataAPI.TradeCalGet(exchangeCD=u"XSHG", beginDate=begin, endDate=end)
cal_dates = cal_dates[cal_dates['isOpen']==1].sort('calendarDate')
# 工作日列表
cal_dates = cal_dates['calendarDate'].values.tolist()
print factor + ' will be calculated for ' + str(len(cal_dates)) + ' days:'
count = 0
secs_time = 0
start_time = time.time()
# 按天拿取因子数据,并保存为一个dataframe
df = pd.DataFrame()
for dt in cal_dates:
# 拿取数据dataapi,必要时可以使用专业版api
dt_df = DataAPI.MktEqudGet(tradeDate=dt, secID='',
field=['tradeDate', 'secID']+[factor])
if df.empty:
df = dt_df
else:
df = df.append(dt_df)
# 打印进度部分,每200天打印一次
if count > 0 and count % 200 == 0:
finish_time = time.time()
print count,
print ' ' + str(np.round((finish_time-start_time) - secs_time, 0)) + ' seconds elapsed.'
secs_time = (finish_time-start_time)
count += 1
# 提取所需的universe对应的因子数据
df = df.set_index(['tradeDate','secID'])[factor].unstack()
if universe:
universe = list(set(universe) & set(df.columns))
df = df[universe]
df.index = pd.to_datetime(df.index, format='%Y-%m-%d')
# # 将上市不满三个月的股票的因子设置为NaN
equ_info = DataAPI.EquGet(equTypeCD=u"A",secID=u"",ticker=u"",listStatusCD=u"",field=u"",pandas="1")
equ_info = equ_info[['secID', 'listDate', 'delistDate']].set_index('secID')
equ_info['delistDate'] = [x if type(x)==str else end for x in equ_info['delistDate']]
equ_info['listDate'] = pd.to_datetime(equ_info['listDate'], format='%Y-%m-%d')
equ_info['delistDate'] = pd.to_datetime(equ_info['delistDate'], format='%Y-%m-%d')
equ_info['listDate'] = [x + timedelta(90) for x in equ_info['listDate']]
for sec in df.columns:
if sec[0] not in '036':
continue
sec_info = equ_info.ix[sec]
df.loc[:sec_info['listDate'], sec] = np.NaN
if end > sec_info['delistDate'].strftime('%Y%m%d'):
df.loc[sec_info['delistDate']:, sec] = np.NaN
return df
这里稍微对获取因子的数据进行了修改。
daily_return = getUqerStockFactors('20090101', '20170303', 'chgPct', universe)
daily_return.to_csv('Full_A_chgPct.csv')


在获得每日涨跌数据后就可以进行因子数据的计算了。
three_month_mean = pd.rolling_mean(daily_return, 60)
three_month_mean.tail()
# 计算上行波动率up_vol_data = pd.DataFrame()for i in range(len(daily_return)-120): temp1 = daily_return[60+i:120+i] temp2 = three_month_mean[60+i:120+i] I_i = temp1 > temp2 up_vol_temp = (((temp1-temp2)*(temp1-temp2))[I_i]).mean() temp = pd.DataFrame(up_vol_temp).T temp.index = [temp1.index[-1]] up_vol_data = up_vol_data.append(temp)up_vol_data.tail(5)

# 计算下行波动率down_vol_data = pd.DataFrame()for i in range(len(daily_return)-120): temp1 = daily_return[60+i:120+i] temp2 = three_month_mean[60+i:120+i] I_i = temp1 < temp2 down_vol_temp = (((temp1-temp2)*(temp1-temp2))[I_i]).mean() temp = pd.DataFrame(down_vol_temp).T temp.index = [temp1.index[-1]] down_vol_data = down_vol_data.append(temp)down_vol_data.tail(5)
factor_df = down_vol_data/up_vol_datafactor_df = np.log(factor_df)factor_df.to_csv('DUVOL.csv')
加载数据文件
duvol_data = pd.read_csv('DUVOL.csv').set_index('Unnamed: 0')duvol_data.index = pd.to_datetime(duvol_data.index)forward_5d_return_data = pd.read_csv('ForwardReturns_W5_FullA.csv') # 未来5天收益率 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') # 市值数据forward_5d_return_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, forward_5d_return_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']))forward_5d_return_data = forward_5d_return_data[forward_5d_return_data.columns[1:]].set_index(forward_5d_return_data['tradeDate'])forward_20d_return_data = forward_20d_return_data[forward_20d_return_data.columns[1:]].set_index('tradeDate')
2 因子分析
2.1 因子的截面特征
n_quantile = 10# 统计十分位数cols_mean = ['meanQ'+str(i+1) for i in range(n_quantile)]cols = cols_meancorr_means = pd.DataFrame(index=duvol_data.index, columns=cols)# 计算相关系数分组平均值for dt in corr_means.index: qt_mean_results = [] # 相关系数去掉nan tmp_factor = duvol_data.ix[dt].dropna() pct_quantiles = 1.0/n_quantile for i in range(n_quantile): down = tmp_factor.quantile(pct_quantiles*i) up = tmp_factor.quantile(pct_quantiles*(i+1)) mean_tmp = tmp_factor[(tmp_factor<=up) & (tmp_factor>=down)].mean() qt_mean_results.append(mean_tmp) corr_means.ix[dt] = qt_mean_results# ------------- 因子历史表现作图 ------------------------fig = plt.figure(figsize=(12, 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()
可以看出,因子的分布并不是很稳定,特别是在2015年至2016年,因子出现了较大的波动。
2.2 因子选股的市值分布特征
mkt_value_data = mkt_value_data.set_index('tradeDate')
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:
if dt not in mkt_df.index:
continue
qt_mean_results = []
tmp_factor = signal_df.ix[dt].dropna()
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_factor.quantile(pct_quantiles*i)
up = tmp_factor.quantile(pct_quantiles*(i+1))
i_quantile_index = tmp_factor[(tmp_factor<=up) & (tmp_factor>=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(duvol_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, origin_mkt_means.values, align='center', width=width)
ax1.set_ylim(0.3,0.6)
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()
这个因子有明显的小市值倾向,使用时应当注意。
2.3 因子的预测能力
ic_data = pd.DataFrame(index=duvol_data.index, columns=['IC','pValue'])
# 计算相关系数
for dt in forward_20d_return_data.index[:-5]:
tmp_duvol = duvol_data.ix[dt]
tmp_ret = forward_20d_return_data.ix[dt]
cor = pd.DataFrame(tmp_duvol)
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.spearmanr(cor['corr'],cor['ret']) # 计算秩相关系数 RankIC
ic_data['IC'][dt] = ic
ic_data['pValue'][dt] = p_value
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]))
# 每一天的**DUVOL**和**之后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'相关系数', fontproperties=font, fontsize=16)
ax1.set_xlabel(u'日期', fontproperties=font, fontsize=16)
ax1.set_title(u"DUVOL和之后20日收益的秩相关系数", fontproperties=font, fontsize=16)
ax1.grid()

可以看出,该因子对20日之后的收益具有一定的预测能力。
2.4 DUVOL选股的分组超额收益
n_quantile = 10
# 统计十分位数
cols_mean = [i+1 for i in range(n_quantile)]
cols = cols_mean
excess_returns_means = pd.DataFrame(index=duvol_data.index, columns=cols)
# 计算DUVOL分组的超额收益平均值
for dt in forward_5d_return_data.index[:-5]:
qt_mean_results = []
# ILLIQ去掉nan
dt = dt.strftime("%Y-%m-%d")
tmp_duvol = duvol_data.ix[dt].dropna()
tmp_return = forward_5d_return_data.ix[dt].dropna()
tmp_return = tmp_return[tmp_return<0.6]
tmp_return_mean = tmp_return.mean()
pct_quantiles = 1.0/n_quantile
for i in range(n_quantile):
down = tmp_duvol.quantile(pct_quantiles*i)
up = tmp_duvol.quantile(pct_quantiles*(i+1))
i_quantile_index = tmp_duvol[(tmp_duvol<=up) & (tmp_duvol>=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)
# 中性化后的ILLIQ分组的超额收益作图
fig = plt.figure(figsize=(12, 6))
ax1 = fig.add_subplot(111)
excess_returns_means_dist = excess_returns_means.mean()
excess_dist_plus = excess_returns_means_dist[excess_returns_means_dist>0]
excess_dist_minus = excess_returns_means_dist[excess_returns_means_dist<0]
lns2 = ax1.bar(excess_dist_plus.index, excess_dist_plus.values, align='center', color='r', width=0.35)
lns3 = ax1.bar(excess_dist_minus.index, excess_dist_minus.values, align='center', color='g', width=0.35)
ax1.set_xlim(left=0.5, right=len(excess_returns_means_dist)+0.5)
# ax1.set_ylim(-0.008, 0.008)
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"DUVOL因子超额收益", fontproperties=font, fontsize=16)
ax1.grid()

2.5 DUVOl因子选股的一个月反转分布特征
backward_20d_return_data.index = backward_20d_return_data['tradeDate']backward_20d_return_data.index = pd.to_datetime(backward_20d_return_data.index)del backward_20d_return_data['Unnamed: 0'], backward_20d_return_data['Unnamed: 0.1'], backward_20d_return_data['tradeDate']
n_quantile = 10# 统计十分位数cols_mean = [i+1 for i in range(n_quantile)]cols = cols_meanhist_returns_means = pd.DataFrame(index=duvol_data.index, columns=cols)# 计算中性化后的ILLIQ分组的一个月反转分布特征for dt in backward_20d_return_data.index[:-5]: qt_mean_results = [] # ILLIQ去掉nan tmp_duvol = duvol_data.ix[dt].dropna() tmp_return = backward_20d_return_data.ix[dt].dropna() tmp_return_mean = tmp_return.mean() pct_quantiles = 1.0/n_quantile for i in range(n_quantile): down = tmp_duvol.quantile(pct_quantiles*i) up = tmp_duvol.quantile(pct_quantiles*(i+1)) i_quantile_index = tmp_duvol[(tmp_duvol<=up) & (tmp_duvol>=down)].index mean_tmp = tmp_return[i_quantile_index].mean() - tmp_return_mean qt_mean_results.append(mean_tmp) hist_returns_means.ix[dt] = qt_mean_resultshist_returns_means.dropna(inplace=True)# 中性化后的ILLIQ分组的一个月反转分布特征作图fig = plt.figure(figsize=(12, 6))ax1 = fig.add_subplot(111)ax2 = ax1.twinx()hist_returns_means_dist = hist_returns_means.mean()lns1 = ax1.bar(hist_returns_means_dist.index, hist_returns_means_dist.values, align='center', width=0.35)lns2 = ax2.plot(excess_returns_means_dist.index, excess_returns_means_dist.values, 'o-r')ax1.legend(lns1, ['20 day return(left axis)'], loc=2, fontsize=12)ax2.legend(lns2, ['excess return(right axis)'], fontsize=12)ax1.set_xlim(left=0.5, right=len(hist_returns_means_dist)+0.5)ax1.set_ylabel(u'历史一个月收益率', fontproperties=font, fontsize=16)ax2.set_ylabel(u'超额收益', fontproperties=font, fontsize=16)ax1.set_xlabel(u'十分位分组', fontproperties=font, fontsize=16)ax1.set_xticks(hist_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)+'%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)ax2.set_yticklabels([str(x*100)+'0%' for x in ax2.get_yticks()], fontproperties=font, fontsize=14)ax1.set_title(u"DUVOL选股因子一个月历史收益率(一个月反转因子)分布特征", fontproperties=font, fontsize=16)ax1.grid()

从这里看出,该因子与反转因子有一定的相关性。
# 对因子进行中性化
duvol_neutral_data = factor_data.copy(deep=True)
for dt in factor_data.index:
dt_str = dt.replace('-', '')
duvol_neutral_data.ix[dt] = pd.Series(neutralize(duvol_data.ix[dt].to_dict(), target_date=dt_str))
duvol_neutral_data.to_csv('duvol_neutral.csv')
# 中性化后该因子基本上没有alpha
4 因子历史回测净值表现
4.1 简单做多策略
接下来,考察因子的选股能力的回测效果。历史回测的基本设置如下:
- 回测时段为2010年1月1日至2017年2月28日
- 股票池为A股全部股票,剔除上市未满60日的新股(计算因子时已剔除);
- 组合每5个交易日调仓,交易费率设为双边千分之二
- 调仓时,涨停、停牌不买入,跌停、停牌不卖出;
- 每次调仓时,选择股票池中因子**最大的10%**的股票;
start = '2010-01-01' # 回测起始时间
end = '2017-02-28' # 回测结束时间
benchmark = 'ZZ500' # 策略参考标准
universe = DynamicUniverse('A') # 证券池,支持股票和基金
capital_base = 10000000 # 起始资金
freq = 'd' # 策略类型,'d'表示日间策略使用日线回测
refresh_rate = 5 # 调仓频率,表示执行handle_data的时间间隔
factor_data = pd.read_csv('DUVOL.csv') # 读取因子数据
factor_data = factor_data[factor_data.columns[:]].set_index('Unnamed: 0')
factor_dates = factor_data.index.values
quantile_ten = 10 # 选取股票的因子十分位数,1表示选取股票池中因子最小的10%的股票
commission = Commission(0.001, 0.001) # 交易费率设为双边千分之二
def initialize(account): # 初始化虚拟账户状态
pass
def handle_data(account): # 每个交易日的买入卖出指令
pre_date = account.previous_date.strftime("%Y-%m-%d")
if pre_date not in factor_dates: # 因子只在每个月底计算,所以调仓也在每月最后一个交易日进行
return
# 拿取调仓日前一个交易日的因子,并按照相应十分位选择股票
q = factor_data.ix[pre_date].dropna()
q_min = q.quantile((quantile_ten-1)*0.1)
q_max = q.quantile(quantile_ten*0.1)
my_univ = q[q>=q_min][q<q_max].index.values
# 调仓逻辑
univ = [x for x in my_univ if x in account.universe]
if len(univ) < 10:
return
# 不在目标股票池中的,卖出
for s in account.valid_secpos:
if s not in univ:
order_to(s, 0)
# 在目标股票池中的,等权买入
buylist = {}
v = account.reference_portfolio_value * 1.05 / len(univ)
for s in univ:
buylist[s] = v / account.reference_price[s] - account.security_position.get(s, 0)
for s in sorted(buylist, key=buylist.get):
order(s, buylist[s])
fig = plt.figure(figsize=(12,5))fig.set_tight_layout(True)ax1 = fig.add_subplot(111)ax2 = ax1.twinx()ax1.grid()bt_quantile_ten = btdata = bt_quantile_ten[[u'tradeDate',u'portfolio_value',u'benchmark_return']]data['portfolio_return'] = data.portfolio_value/data.portfolio_value.shift(1) - 1.0data['portfolio_return'].ix[0] = data['portfolio_value'].ix[0]/ 10000000.0 - 1.0data['excess_return'] = data.portfolio_return - data.benchmark_returndata['excess'] = data.excess_return + 1.0data['excess'] = data.excess.cumprod()data['portfolio'] = data.portfolio_return + 1.0data['portfolio'] = data.portfolio.cumprod()data['benchmark'] = data.benchmark_return + 1.0data['benchmark'] = data.benchmark.cumprod()# ax.plot(data[['portfolio','benchmark','excess']], label=str(qt))ax1.plot(data['tradeDate'], data[['portfolio']], label='portfolio(left)')ax1.plot(data['tradeDate'], data[['benchmark']], label='benchmark(left)')ax2.plot(data['tradeDate'], data[['excess']], label='hedged(right)', color='r')ax1.legend(loc=2)ax2.legend(loc=0)ax2.set_ylim(bottom=1.0, top=3)ax1.set_ylabel(u"净值", fontproperties=font, fontsize=16)ax2.set_ylabel(u"对冲指数净值", fontproperties=font, fontsize=16)ax2.set_ylabel(u"对冲指数净值", fontproperties=font, fontsize=16)ax1.set_title(u"因子最大的10%股票周度调仓走势", fontproperties=font, fontsize=16)

4.2 因子选股 —— 不同五分位数组合回测走势比较
# 可编辑部分与 strategy 模式一样,其余部分按本例代码编写即可# -----------回测参数部分开始,可编辑------------start = '2010-01-01' # 回测起始时间end = '2016-12-30' # 回测结束时间benchmark = 'ZZ500' # 策略参考标准universe = DynamicUniverse('A') # 证券池,支持股票和基金capital_base = 10000000 # 起始资金freq = 'd' # 策略类型,'d'表示日间策略使用日线回测refresh_rate = 5 # 调仓频率,表示执行handle_data的时间间隔factor_data = pd.read_csv('DUVOL.csv') # 读取因子数据factor_data = factor_data[factor_data.columns[:]].set_index('Unnamed: 0')q_dates = factor_data.index.valuesquantile_five = 1 # 选取股票的因子十分位数,1表示选取股票池中因子最小的10%的股票commission = Commission(0.0005,0.0005) # 交易费率设为双边千分之一# ---------------回测参数部分结束----------------# 把回测参数封装到 SimulationParameters 中,供 quick_backtest 使用sim_params = quartz.SimulationParameters(start, end, benchmark, universe, capital_base)# 获取回测行情数据idxmap, data = quartz.get_daily_data(sim_params)# 运行结果results = {}# 调整参数(选取股票的Q因子五分位数),进行快速回测for quantile_five in range(1, 6): # ---------------策略逻辑部分---------------- refresh_rate = 5 commission = Commission(0.0005,0.0005) def initialize(account): # 初始化虚拟账户状态 pass def handle_data(account): # 每个交易日的买入卖出指令 pre_date = account.previous_date.strftime("%Y-%m-%d") if pre_date not in q_dates: # 因子只在每个月底计算,所以调仓也在每月最后一个交易日进行 return # 拿取调仓日前一个交易日的因子,并按照相应十分位选择股票 q = factor_data.ix[pre_date].dropna() q_min = q.quantile((quantile_five-1)*0.2) q_max = q.quantile(quantile_five*0.2) my_univ = q[q>=q_min][q<q_max].index.values # 调仓逻辑 univ = [x for x in my_univ if x in account.universe] if len(univ) < 10: return # 不在目标股票池中的,卖出 for s in account.valid_secpos: if s not in univ: order_to(s, 0) # 在目标股票池中的,等权买入 buylist = {} v = account.referencePortfolioValue * 1.05 / len(univ) for s in univ: buylist[s] = v / account.reference_price[s] - account.valid_secpos.get(s, 0) for s in sorted(buylist, key=buylist.get): order(s, buylist[s]) # ---------------策略逻辑部分结束---------------- # 把回测逻辑封装到 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[quantile_five] = tmp print str(quantile_five),print '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: 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)ax2.legend(loc=0)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 转换为 DataFrameimport pandasresults_pd = pandas.DataFrame(results).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


上图显示出,因子选股不同五分位构建等权组合,在uqer进行真实回测的净值曲线;显示出因子的选股能力,不同五分位组合净值曲线随时间推移逐渐散开。
5 总结
- 综合来看,DUVOL的因子有一定的选股能力,因子最大的10分位股票构建的组合alpha为14%,IR为1.75。
发布者:股市刺客,转载请注明出处:https://www.95sca.cn/archives/321271
站内所有文章皆来自网络转载或读者投稿,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。敬请谅解!