分享

凤鸣朝阳

 量化猫 2018-02-23

1. 概述

参考方正金工最近的研究报告『凤鸣朝阳:股价日内模式中蕴藏的选股因子』作者:魏建榕,在本文做了一些实现:

  • 七年回测中,简单多头策略年化收益率39.8%,阿尔法25.6%,夏普比率1.21,信息比率2.89

2. 因子计算

市场交易其实是在交易者基于已知信息的基础上达成的,而晚上闭市之后产生的各种信息,可能在第二天开盘时达到集中释放。如何去通过市场交易特征去掌握这种信息的集中释放,一篇文章Exploring Market State and Stock Interactions on the Minute Timescale给了我们很大的启发。 作者在该文章中得到了如图的结果

image

中蓝线是个股之间相关系数的平均值,不难发现,下午个股之间的相关性要显著地高于上午。也就是说,上午会集中释放前夜的信息,上午是知情交易者最活跃的时候。怎么样利用这一特征呢,方正的研报中给出了一种方法凤鸣朝阳

本文中就利用这种方法构造选股因子。

xxxxxxxxxx
13
1
from datetime import datetime, timedelta
2
import matplotlib.pyplot as plt
3
from matplotlib import rc
4
rc('mathtext', default='regular')
5
import seaborn as sns
6
sns.set_style('white')
7
from matplotlib import dates
8
import numpy as np
9
import pandas as pd
10
import statsmodels.api as sm
11
import time
12
import scipy.stats as st
13
from CAL.PyCAL import *    # CAL.PyCAL中包含font
查看全部
xxxxxxxxxx
9
1
# 已经将上下午的涨跌幅进行读取保存为文件,方便直接读取
2
3
apmret = pd.read_csv('APM_Ret.csv')
4
5
apmret = apmret.set_index(['tradeDate', 'secID']).stack().unstack(level=1)
6
apm = pd.DataFrame(index=apmret.index.levels[0], columns=apmret.columns)
7
apmret = apmret.apply(lambda x: [ np.NaN if (xx == np.inf) else xx for xx in x])
8
9
apmret['mktRet'] = apmret.mean(axis=1)
查看全部

按照研报中的方法计算因子

xxxxxxxxxx
34
1
window = 20
2
3
count = 0
4
for dt in apm.index:
5
        
6
    count += 1
7
    if count % 100 == 0:
8
        print dt
9
        
10
    # 拿取20天的上下午涨跌幅度数据
11
    dt_data = apmret[apmret.index.get_level_values('tradeDate')<=dt].tail(window*2)
12
    if len(dt_data) < window*2:
13
        continue
14
        
15
    # 股票涨跌幅和市场涨跌幅
16
    # 市场涨跌幅为所有股票涨跌幅的简单平均
17
    stk_ret = dt_data.drop('mktRet', axis=1).values
18
    mkt_ret = dt_data['mktRet'].values
19
    
20
    # 上午、下午涨跌幅和市场数据进行回归
21
    aa = np.matrix([np.ones_like(range(window*2)), mkt_ret]).T
22
    bb = stk_ret
23
    xx = np.linalg.lstsq(aa, bb)[0]
24
    residuals = bb - aa.dot(xx)
25
    
26
    # 上午和下午的回归残差相减
27
    delta = residuals[0::2,:] - residuals[1::2,:]
28
    
29
    # stat衡量上下午残差的差异程度
30
    stat = np.nanmean(delta, axis=0) * np.sqrt(window) / np.nanstd(delta, axis=0)
31
    
32
    apm.ix[dt] = stat
33
    
34
apm.to_csv('APM_FullA.csv')
查看全部

3. 因子截面特征

3.1 加载数据文件

xxxxxxxxxx
18
1
# 提取数据
2
factor_data = pd.read_csv('APM_FullA.csv')                      # 选股因子
3
factor_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, factor_data['tradeDate']))
4
factor_data = factor_data[factor_data.columns[:]].set_index('tradeDate')
5
6
mkt_value_data = pd.read_csv('MarketValues_FullA.csv')                    # 市值数据
7
mkt_value_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, mkt_value_data['tradeDate']))
8
mkt_value_data = mkt_value_data[mkt_value_data.columns[1:]].set_index('tradeDate')
9
10
forward_20d_return_data = pd.read_csv('ForwardReturns_W20_FullA.csv')    # 未来20天收益率 
11
forward_20d_return_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, forward_20d_return_data['tradeDate']))
12
forward_20d_return_data = forward_20d_return_data[forward_20d_return_data.columns[1:]].set_index('tradeDate')
13
14
backward_20d_return_data = pd.read_csv('BackwardReturns_W20_FullA.csv')  # 过去20天收益率 
15
backward_20d_return_data['tradeDate'] = map(Date.toDateTime, map(DateTime.parseISO, backward_20d_return_data['tradeDate']))
16
backward_20d_return_data = backward_20d_return_data[backward_20d_return_data.columns[1:]].set_index('tradeDate')
17
18
factor_data[factor_data.columns[0:5]].tail()
查看全部
000001.XSHE 000002.XSHE 000004.XSHE 000005.XSHE 000006.XSHE
tradeDate
2016-10-10 -0.595197 -0.175170 NaN 0.750578 1.420056
2016-10-11 -0.359962 -0.049394 NaN 0.959939 1.551163
2016-10-12 -0.221620 0.045939 NaN 0.834986 1.397590
2016-10-13 -0.380221 0.212967 NaN 1.119334 1.607908
2016-10-14 0.029359 0.197239 NaN 0.967538 1.410072

3.2 因子截面特征

xxxxxxxxxx
40
1
# 因子历史表现
2
3
n_quantile = 10
4
# 统计十分位数
5
cols_mean = ['meanQ'+str(i+1) for i in range(n_quantile)]
6
cols = cols_mean
7
corr_means = pd.DataFrame(index=factor_data.index, columns=cols)
8
9
# 计算相关系数分组平均值
10
for dt in corr_means.index:
11
    qt_mean_results = []
12
13
    # 相关系数去掉nan
14
    tmp_factor = factor_data.ix[dt].dropna()
15
    
16
    pct_quantiles = 1.0/n_quantile
17
    for i in range(n_quantile):
18
        down = tmp_factor.quantile(pct_quantiles*i)
19
        up = tmp_factor.quantile(pct_quantiles*(i+1))
20
        mean_tmp = tmp_factor[(tmp_factor<=up) & (tmp_factor>=down)].mean()
21
        qt_mean_results.append(mean_tmp)
22
    corr_means.ix[dt] = qt_mean_results
23
24
25
# ------------- 因子历史表现作图 ------------------------
26
27
fig = plt.figure(figsize=(12, 6))
28
ax1 = fig.add_subplot(111)
29
30
lns1 = ax1.plot(corr_means.index, corr_means.meanQ1, label='Q1')
31
lns2 = ax1.plot(corr_means.index, corr_means.meanQ5, label='Q5')
32
lns3 = ax1.plot(corr_means.index, corr_means.meanQ10, label='Q10')
33
34
lns = lns1+lns2+lns3
35
labs = [l.get_label() for l in lns]
36
ax1.legend(lns, labs, bbox_to_anchor=[0.5, 0.1], loc='', ncol=3, mode="", borderaxespad=0., fontsize=12)
37
ax1.set_ylabel(u'因子', fontproperties=font, fontsize=16)
38
ax1.set_xlabel(u'日期', fontproperties=font, fontsize=16)
39
ax1.set_title(u"因子历史表现", fontproperties=font, fontsize=16)
40
ax1.grid()
查看全部

可以发现,其实股灾时候这个因子的值也出现一些异常,毕竟是千古跌停奇观

3.3 因子预测能力初探

接下来,我们计算了每一天的因子和之后20日收益的秩相关系数

xxxxxxxxxx
46
1
# 计算了每一天的**因子**和**之后20日收益**的秩相关系数
2
3
ic_data = pd.DataFrame(index=factor_data.index, columns=['IC','pValue'])
4
5
# 计算相关系数
6
for dt in ic_data.index:
7
    if dt not in forward_20d_return_data.index:
8
        continue
9
        
10
    tmp_factor = factor_data.ix[dt]
11
    tmp_ret = forward_20d_return_data.ix[dt]
12
    fct = pd.DataFrame(tmp_factor)
13
    ret = pd.DataFrame(tmp_ret)
14
    fct.columns = ['fct']
15
    ret.columns = ['ret']
16
    fct['ret'] = ret['ret']
17
    fct = fct[~np.isnan(fct['fct'])][~np.isnan(fct['ret'])]
18
    if len(fct) < 5:
19
        continue
20
21
    ic, p_value = st.spearmanr(fct['fct'],fct['ret'])   # 计算秩相关系数 RankIC
22
    ic_data['IC'][dt] = ic
23
    ic_data['pValue'][dt] = p_value
24
    
25
ic_data.dropna(inplace=True)    
26
27
print 'mean of IC: ', ic_data['IC'].mean(), ';',
28
print 'median of IC: ', ic_data['IC'].median()
29
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]))
30
31
32
# 每一天的**因子**和**之后20日收益**的秩相关系数作图
33
34
fig = plt.figure(figsize=(16, 6))
35
ax1 = fig.add_subplot(111)
36
37
lns1 = ax1.plot(ic_data[ic_data>0].index, ic_data[ic_data>0].IC, '.r', label='IC(plus)')
38
lns2 = ax1.plot(ic_data[ic_data<0].index, ic_data[ic_data<0].IC, '.b', label='IC(minus)')
39
40
lns = lns1+lns2
41
labs = [l.get_label() for l in lns]
42
ax1.legend(lns, labs, bbox_to_anchor=[0.6, 0.1], loc='', ncol=2, mode="", borderaxespad=0., fontsize=12)
43
ax1.set_ylabel(u'相关系数', fontproperties=font, fontsize=16)
44
ax1.set_xlabel(u'日期', fontproperties=font, fontsize=16)
45
ax1.set_title(u"因子和之后20日收益的秩相关系数", fontproperties=font, fontsize=16)
46
ax1.grid()
查看全部
mean of IC:
0.0216027879304 ; median of IC: 0.0239615147292 the number of IC(all, plus, minus): (1804, 1121, 683)

从上面计算结果可知,该因子和之后20日收益的秩相关系数在大部分时间为正,因子对之后20日收益有预测性

4. 历史回测概述

本节使用2009年以来的数据对于该选股因子进行回测,进一步简单涉及几个风险因子暴露情况

4.1 该因子选股的分组超额收益(月度)

xxxxxxxxxx
49
1
n_quantile = 10
2
# 统计十分位数
3
cols_mean = [i+1 for i in range(n_quantile)]
4
cols = cols_mean
5
6
excess_returns_means = pd.DataFrame(index=factor_data.index, columns=cols)
7
8
# 计算因子分组的超额收益平均值
9
for dt in excess_returns_means.index:
10
    if dt not in forward_20d_return_data.index:
11
        continue 
12
    qt_mean_results = []
13
    
14
    tmp_factor = factor_data.ix[dt].dropna()
15
    tmp_return = forward_20d_return_data.ix[dt].dropna()
16
    tmp_return = tmp_return[tmp_return<4.0]
17
    tmp_return_mean = tmp_return.mean()
18
    
19
    pct_quantiles = 1.0/n_quantile
20
    for i in range(n_quantile):
21
        down = tmp_factor.quantile(pct_quantiles*i)
22
        up = tmp_factor.quantile(pct_quantiles*(i+1))
23
        i_quantile_index = tmp_factor[(tmp_factor<=up) & (tmp_factor>=down)].index
24
        mean_tmp = tmp_return[i_quantile_index].mean() - tmp_return_mean
25
        qt_mean_results.append(mean_tmp)
26
        
27
    excess_returns_means.ix[dt] = qt_mean_results
28
29
excess_returns_means.dropna(inplace=True)
30
31
# 因子分组的超额收益作图
32
fig = plt.figure(figsize=(12, 6))
33
ax1 = fig.add_subplot(111)
34
35
excess_returns_means_dist = excess_returns_means.mean()
36
excess_dist_plus = excess_returns_means_dist[excess_returns_means_dist>0]
37
excess_dist_minus = excess_returns_means_dist[excess_returns_means_dist<0]
38
lns2 = ax1.bar(excess_dist_plus.index, excess_dist_plus.values, align='center', color='r', width=0.35)
39
lns3 = ax1.bar(excess_dist_minus.index, excess_dist_minus.values, align='center', color='g', width=0.35)
40
41
ax1.set_xlim(left=0.5, right=len(excess_returns_means_dist)+0.5)
42
# ax1.set_ylim(-0.008, 0.008)
43
ax1.set_ylabel(u'超额收益', fontproperties=font, fontsize=16)
44
ax1.set_xlabel(u'十分位分组', fontproperties=font, fontsize=16)
45
ax1.set_xticks(excess_returns_means_dist.index)
46
ax1.set_xticklabels([int(x) for x in ax1.get_xticks()], fontproperties=font, fontsize=14)
47
ax1.set_yticklabels([str(x*100)+'0%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)
48
ax1.set_title(u"因子选股分组超额收益", fontproperties=font, fontsize=16)
49
ax1.grid()
查看全部

可以看到,该因子选股不同分位数组合的超额收益呈很好的单调性;因子空头收益更显著

4.2 因子选股的市值分布特征

检查因子的小市值暴露情况。因为很多策略因为小市值暴露在A股市场表现优异。

xxxxxxxxxx
50
1
# 计算因子分组的市值分位数平均值
2
def quantile_mkt_values(signal_df, mkt_df):
3
    n_quantile = 10
4
    # 统计十分位数
5
    cols_mean = [i+1 for i in range(n_quantile)]
6
    cols = cols_mean
7
8
    mkt_value_means = pd.DataFrame(index=signal_df.index, columns=cols)
9
10
    # 计算相关系数分组的市值分位数平均值
11
    for dt in mkt_value_means.index:
12
        if dt not in mkt_df.index:
13
            continue 
14
        qt_mean_results = []
15
16
        # 相关系数去掉nan和绝对值大于0.97的
17
        tmp_factor = signal_df.ix[dt].dropna()
18
        tmp_mkt_value = mkt_df.ix[dt].dropna()
19
        tmp_mkt_value = tmp_mkt_value.rank()/len(tmp_mkt_value)
20
21
        pct_quantiles = 1.0/n_quantile
22
        for i in range(n_quantile):
23
            down = tmp_factor.quantile(pct_quantiles*i)
24
            up = tmp_factor.quantile(pct_quantiles*(i+1))
25
            i_quantile_index = tmp_factor[(tmp_factor<=up) & (tmp_factor>=down)].index
26
            mean_tmp = tmp_mkt_value[i_quantile_index].mean()
27
            qt_mean_results.append(mean_tmp)
28
        mkt_value_means.ix[dt] = qt_mean_results
29
    mkt_value_means.dropna(inplace=True)
30
    return mkt_value_means.mean()
31
    
32
# 计算因子分组的市值分位数平均值
33
origin_mkt_means = quantile_mkt_values(factor_data, mkt_value_data)
34
35
# 因子分组的市值分位数平均值作图
36
fig = plt.figure(figsize=(12, 6))
37
ax1 = fig.add_subplot(111)
38
39
width = 0.3
40
lns1 = ax1.bar(origin_mkt_means.index, origin_mkt_means.values, align='center', width=width)
41
42
ax1.set_ylim(0.3,0.6)
43
ax1.set_xlim(left=0.5, right=len(origin_mkt_means)+0.5)
44
ax1.set_ylabel(u'市值百分位数', fontproperties=font, fontsize=16)
45
ax1.set_xlabel(u'十分位分组', fontproperties=font, fontsize=16)
46
ax1.set_xticks(origin_mkt_means.index)
47
ax1.set_xticklabels([int(x) for x in ax1.get_xticks()], fontproperties=font, fontsize=14)
48
ax1.set_yticklabels([str(x*100)+'0%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)
49
ax1.set_title(u"因子分组市值分布特征", fontproperties=font, fontsize=16)
50
ax1.grid()
查看全部

上图展示,该选股因子并没有明显的小市值暴露;倒是多头组合(第十分位组合)市值略大

4.3 因子分组选股的一个月反转分布特征

xxxxxxxxxx
50
1
n_quantile = 10
2
# 统计十分位数
3
cols_mean = [i+1 for i in range(n_quantile)]
4
cols = cols_mean
5
hist_returns_means = pd.DataFrame(index=factor_data.index, columns=cols)
6
7
# 因子分组的一个月反转分布特征
8
for dt in hist_returns_means.index:
9
    if dt not in backward_20d_return_data.index:
10
        continue 
11
    qt_mean_results = []
12
    
13
    # 去掉nan
14
    tmp_factor = factor_data.ix[dt].dropna()
15
    tmp_return = backward_20d_return_data.ix[dt].dropna()
16
    tmp_return_mean = tmp_return.mean()
17
    
18
    pct_quantiles = 1.0/n_quantile
19
    for i in range(n_quantile):
20
        down = tmp_factor.quantile(pct_quantiles*i)
21
        up = tmp_factor.quantile(pct_quantiles*(i+1))
22
        i_quantile_index = tmp_factor[(tmp_factor<=up) & (tmp_factor>=down)].index
23
        mean_tmp = tmp_return[i_quantile_index].mean() - tmp_return_mean
24
        qt_mean_results.append(mean_tmp)
25
        
26
    hist_returns_means.ix[dt] = qt_mean_results
27
28
hist_returns_means.dropna(inplace=True)
29
30
# 一个月反转分布特征作图
31
fig = plt.figure(figsize=(12, 6))
32
ax1 = fig.add_subplot(111)
33
ax2 = ax1.twinx()
34
35
hist_returns_means_dist = hist_returns_means.mean()
36
lns1 = ax1.bar(hist_returns_means_dist.index, hist_returns_means_dist.values, align='center', width=0.35)
37
lns2 = ax2.plot(excess_returns_means_dist.index, excess_returns_means_dist.values, 'o-r')
38
39
ax1.legend(lns1, ['20 day return(left axis)'], loc=2, fontsize=12)
40
ax2.legend(lns2, ['excess return(right axis)'], fontsize=12)
41
ax1.set_xlim(left=0.5, right=len(hist_returns_means_dist)+0.5)
42
ax1.set_ylabel(u'历史一个月收益率', fontproperties=font, fontsize=16)
43
ax2.set_ylabel(u'超额收益', fontproperties=font, fontsize=16)
44
ax1.set_xlabel(u'十分位分组', fontproperties=font, fontsize=16)
45
ax1.set_xticks(hist_returns_means_dist.index)
46
ax1.set_xticklabels([int(x) for x in ax1.get_xticks()], fontproperties=font, fontsize=14)
47
ax1.set_yticklabels([str(x*100)+'%' for x in ax1.get_yticks()], fontproperties=font, fontsize=14)
48
ax2.set_yticklabels([str(x*100)+'0%' for x in ax2.get_yticks()], fontproperties=font, fontsize=14)
49
ax1.set_title(u"因子选股一个月历史收益率(一个月反转因子)分布特征", fontproperties=font, fontsize=16)
50
ax1.grid()
查看全部

可以看出,因子和反转因子的相关性较强

5. 因子历史回测净值表现

5.1 简单做多策略

接下来,考察因子的选股能力的回测效果。历史回测的基本设置如下:

  • 回测时段为2009年3月1日至2016年10月12日
  • 股票池为A股全部股票,剔除上市未满60日的新股(计算因子时已剔除);
  • 组合每10个交易日调仓,交易费率设为双边万分之二
  • 调仓时,涨停、停牌不买入,跌停、停牌不卖出;
  • 每次调仓时,选择股票池中**因子最大的10%**的股票;
xxxxxxxxxx
40
1
start = '2009-03-01'                       # 回测起始时间
2
end = '2016-10-12'                         # 回测结束时间
3
4
benchmark = 'ZZ500'                        # 策略参考标准
5
universe = set_universe('A')               # 证券池,支持股票和基金
6
capital_base = 10000000                    # 起始资金
7
freq = 'd'                                 # 策略类型,'d'表示日间策略使用日线回测
8
refresh_rate = 10                           # 调仓频率,表示执行handle_data的时间间隔
9
10
factor_data = pd.read_csv('APM_FullA.csv')     # 读取因子数据
11
factor_data = factor_data[factor_data.columns[:]].set_index('tradeDate')
12
factor_dates = factor_data.index.values
13
14
quantile_ten = 10                           # 选取股票的因子十分位数,1表示选取股票池中因子最小的10%的股票
15
commission = Commission(0.0002,0.0002)     # 交易费率设为双边万分之二
16
17
def initialize(account):                   # 初始化虚拟账户状态
18
    pass
19
20
def handle_data(account):                  # 每个交易日的买入卖出指令
21
    pre_date = account.previous_date.strftime("%Y-%m-%d")
22
    if pre_date not in factor_dates:            # 因子只在每个月底计算,所以调仓也在每月最后一个交易日进行
23
        return
24
    
25
    # 拿取调仓日前一个交易日的因子,并按照相应十分位选择股票
26
    q = factor_data.ix[pre_date].dropna()
27
    q_min = q.quantile((quantile_ten-1)*0.1)
28
    q_max = q.quantile(quantile_ten*0.1)
29
    my_univ = q[q>=q_min][q<q_max].index.values
30
    
31
    # 调仓逻辑
32
    univ = [x for x in my_univ if x in account.universe]
33
    
34
    # 不在股票池中的,清仓
35
    for stk in account.valid_secpos:
36
        if stk not in univ:
37
            order_to(stk, 0)
38
    # 在目标股票池中的,等权买入
39
    for stk in univ:
40
        order_pct_to(stk, 1.1/len(univ))
查看全部
  • 年化收益率39.8%
  • 基准年化收益率14.2%
  • 阿尔法25.6%
  • 贝塔0.99
  • 夏普比率1.21
  • 收益波动率30.3%
  • 信息比率2.89
  • 最大回撤51.1%
  • 换手率113.62
Created with Highstock 1.3.10累计收益率策略基准20102011201220132014201520160.00%500.00%1000.00%1500.00%-500.00%2000.00%
WARNING: account.valid_secpos is depreciated, please use account.security_position instead.
xxxxxxxxxx
29
1
fig = plt.figure(figsize=(12,5))
2
fig.set_tight_layout(True)
3
ax1 = fig.add_subplot(111)
4
ax2 = ax1.twinx()
5
ax1.grid()
6
7
bt_quantile_ten = bt
8
data = bt_quantile_ten[[u'tradeDate',u'portfolio_value',u'benchmark_return']]
9
data['portfolio_return'] = data.portfolio_value/data.portfolio_value.shift(1) - 1.0
10
data['portfolio_return'].ix[0] = data['portfolio_value'].ix[0]/ 10000000.0 - 1.0
11
data['excess_return'] = data.portfolio_return - data.benchmark_return
12
data['excess'] = data.excess_return + 1.0
13
data['excess'] = data.excess.cumprod()
14
data['portfolio'] = data.portfolio_return + 1.0
15
data['portfolio'] = data.portfolio.cumprod()
16
data['benchmark'] = data.benchmark_return + 1.0
17
data['benchmark'] = data.benchmark.cumprod()
18
# ax.plot(data[['portfolio','benchmark','excess']], label=str(qt))
19
ax1.plot(data['tradeDate'], data[['portfolio']], label='portfolio(left)')
20
ax1.plot(data['tradeDate'], data[['benchmark']], label='benchmark(left)')
21
ax2.plot(data['tradeDate'], data[['excess']], label='hedged(right)', color='r')
22
23
ax1.legend(loc=2)
24
ax2.legend(loc=0)
25
ax2.set_ylim(bottom=1.0, top=5)
26
ax1.set_ylabel(u"净值", fontproperties=font, fontsize=16)
27
ax2.set_ylabel(u"对冲指数净值", fontproperties=font, fontsize=16)
28
ax2.set_ylabel(u"对冲指数净值", fontproperties=font, fontsize=16)
29
ax1.set_title(u"因子最小的10%股票月度调仓走势", fontproperties=font, fontsize=16)
查看全部
<matplotlib.text.Text at 0x1c447210>

上图显示了简单做多因子最大的10%的股票之后的对冲净值走势,需要注意这里对冲基准为中证500指数

5.2 因子选股 —— 不同五分位数组合回测走势比较

xxxxxxxxxx
82
1
# 可编辑部分与 strategy 模式一样,其余部分按本例代码编写即可
2
3
# -----------回测参数部分开始,可编辑------------
4
start = '2009-03-01'                       # 回测起始时间
5
end = '2016-10-12'                         # 回测结束时间
6
benchmark = 'ZZ500'                        # 策略参考标准
7
universe = set_universe('A')           # 证券池,支持股票和基金
8
capital_base = 10000000                     # 起始资金
9
freq = 'd'                                 # 策略类型,'d'表示日间策略使用日线回测
10
refresh_rate = 10                           # 调仓频率,表示执行handle_data的时间间隔
11
12
factor_data = pd.read_csv('APM_FullA.csv')     # 读取因子数据
13
factor_data = factor_data[factor_data.columns[:]].set_index('tradeDate')
14
q_dates = factor_data.index.values
15
16
quantile_five = 1                           # 选取股票的因子十分位数,1表示选取股票池中因子最小的10%的股票
17
commission = Commission(0.0002,0.0002)     # 交易费率设为双边万分之二
18
# ---------------回测参数部分结束----------------
19
20
21
# 把回测参数封装到 SimulationParameters 中,供 quick_backtest 使用
22
sim_params = quartz.SimulationParameters(start, end, benchmark, universe, capital_base)
23
# 获取回测行情数据
24
idxmap, data = quartz.get_daily_data(sim_params)
25
# 运行结果
26
results = {}
27
28
# 调整参数(选取股票的Q因子五分位数),进行快速回测
29
for quantile_five in range(1, 6):
30
    
31
    # ---------------策略逻辑部分----------------
32
    refresh_rate = 1
33
    commission = Commission(0.0002, 0.0002)
34
    
35
    def initialize(account):                   # 初始化虚拟账户状态
36
        pass
37
38
    def handle_data(account):                  # 每个交易日的买入卖出指令
39
        pre_date = account.previous_date.strftime("%Y-%m-%d")
40
        if pre_date not in q_dates:            # 因子只在每个月底计算,所以调仓也在每月最后一个交易日进行
41
            return
42
43
        # 拿取调仓日前一个交易日的因子,并按照相应十分位选择股票
44
        q = factor_data.ix[pre_date].dropna()
45
        # q = q[q>0]
46
        q_min = q.quantile((quantile_five-1)*0.2)
47
        q_max = q.quantile(quantile_five*0.2)
48
        my_univ = q[q>=q_min][q<q_max].index.values
49
50
        # 调仓逻辑
51
        univ = [x for x in my_univ if x in account.universe]
52
        # 不在股票池中的,清仓
53
        for stk in account.valid_secpos:
54
            if stk not in univ:
55
                order_to(stk, 0)
56
        # 在目标股票池中的,等权买入
57
        for stk in univ:
58
            order_pct_to(stk, 1.01/len(univ))
59
    # ---------------策略逻辑部分结束----------------
60
61
    # 把回测逻辑封装到 TradingStrategy 中,供 quick_backtest 使用
62
    strategy = quartz.TradingStrategy(initialize, handle_data)
63
    # 回测部分
64
    bt, acct = quartz.quick_backtest(sim_params, strategy, idxmap, data, refresh_rate=refresh_rate, commission=commission)
65
66
    # 对于回测的结果,可以通过 perf_parse 函数计算风险指标
67
    perf = quartz.perf_parse(bt, acct)
68
69
    # 保存运行结果
70
    tmp = {}
71
    tmp['bt'] = bt
72
    tmp['annualized_return'] = perf['annualized_return']
73
    tmp['volatility'] = perf['volatility']
74
    tmp['max_drawdown'] = perf['max_drawdown']
75
    tmp['alpha'] = perf['alpha']
76
    tmp['beta'] = perf['beta']
77
    tmp['sharpe'] = perf['sharpe']
78
    tmp['information_ratio'] = perf['information_ratio']
79
    
80
    results[quantile_five] = tmp
81
    print str(quantile_five),
82
print 'done'
查看全部
warning: get_daily_data is depreciated, please use get_backtest_data instead
WARNING: account.valid_secpos is depreciated, please use account.security_position instead.
1
2
3
4
5 done
xxxxxxxxxx
54
1
fig = plt.figure(figsize=(10,8))
2
fig.set_tight_layout(True)
3
ax1 = fig.add_subplot(211)
4
ax2 = fig.add_subplot(212)
5
ax1.grid()
6
ax2.grid()
7
8
for qt in results:
9
    bt = results[qt]['bt']
10
11
    data = bt[[u'tradeDate',u'portfolio_value',u'benchmark_return']]
12
    data['portfolio_return'] = data.portfolio_value/data.portfolio_value.shift(1) - 1.0   # 总头寸每日回报率
13
    data['portfolio_return'].ix[0] = data['portfolio_value'].ix[0]/ 10000000.0 - 1.0
14
    data['excess_return'] = data.portfolio_return - data.benchmark_return                 # 总头寸每日超额回报率
15
    data['excess'] = data.excess_return + 1.0
16
    data['excess'] = data.excess.cumprod()                # 总头寸对冲指数后的净值序列
17
    data['portfolio'] = data.portfolio_return + 1.0     
18
    data['portfolio'] = data.portfolio.cumprod()          # 总头寸不对冲时的净值序列
19
    data['benchmark'] = data.benchmark_return + 1.0
20
    data['benchmark'] = data.benchmark.cumprod()          # benchmark的净值序列
21
    results[qt]['hedged_max_drawdown'] = max([1 - v/max(1, max(data['excess'][:i+1])) for i,v in enumerate(data['excess'])])  # 对冲后净值最大回撤
22
    results[qt]['hedged_volatility'] = np.std(data['excess_return'])*np.sqrt(252)
23
    results[qt]['hedged_annualized_return'] = (data['excess'].values[-1])**(252.0/len(data['excess'])) - 1.0
24
    # data[['portfolio','benchmark','excess']].plot(figsize=(12,8))
25
    # ax.plot(data[['portfolio','benchmark','excess']], label=str(qt))
26
    ax1.plot(data['tradeDate'], data[['portfolio']], label=str(qt))
27
    ax2.plot(data['tradeDate'], data[['excess']], label=str(qt))
28
    
29
30
ax1.legend(loc=0)
31
ax2.legend(loc=0)
32
ax1.set_ylabel(u"净值", fontproperties=font, fontsize=16)
33
ax2.set_ylabel(u"对冲净值", fontproperties=font, fontsize=16)
34
ax1.set_title(u"因子不同五分位数分组选股净值走势", fontproperties=font, fontsize=16)
35
ax2.set_title(u"因子不同五分位数分组选股对冲中证500指数后净值走势", fontproperties=font, fontsize=16)
36
37
# results 转换为 DataFrame
38
import pandas
39
results_pd = pandas.DataFrame(results).T.sort_index()
40
41
results_pd = results_pd[[u'alpha', u'beta', u'information_ratio', u'sharpe', 
42
                        u'annualized_return', u'max_drawdown', u'volatility', 
43
                         u'hedged_annualized_return', u'hedged_max_drawdown', u'hedged_volatility']]
44
45
for col in results_pd.columns:
46
    results_pd[col] = [np.round(x, 3) for x in results_pd[col]]
47
    
48
cols = [(u'风险指标', u'Alpha'), (u'风险指标', u'Beta'), (u'风险指标', u'信息比率'), (u'风险指标', u'夏普比率'),
49
        (u'纯股票多头时', u'年化收益'), (u'纯股票多头时', u'最大回撤'), (u'纯股票多头时', u'收益波动率'), 
50
        (u'对冲后', u'年化收益'), (u'对冲后', u'最大回撤'), 
51
        (u'对冲后', u'收益波动率')]
52
results_pd.columns = pd.MultiIndex.from_tuples(cols)
53
results_pd.index.name = u'五分位组别'
54
results_pd
查看全部
风险指标 纯股票多头时 对冲后
Alpha Beta 信息比率 夏普比率 年化收益 最大回撤 收益波动率 年化收益 最大回撤 收益波动率
五分位组别
1 -0.080 0.944 -1.324 0.086 0.057 0.601 0.287 -0.080 0.475 0.062
2 -0.007 0.978 -0.183 0.341 0.133 0.531 0.295 -0.011 0.234 0.051
3 0.039 0.980 0.701 0.501 0.179 0.505 0.294 0.031 0.090 0.044
4 0.104 0.993 1.950 0.717 0.246 0.518 0.298 0.091 0.130 0.045
5 0.245 0.997 3.132 1.172 0.387 0.555 0.303 0.216 0.189 0.063

上图显示出,因子选股不同五分位构建等权组合,在uqer进行真实回测的净值曲线;显示出因子很强的选股能力,不同五分位组合净值曲线随时间推移逐渐散开。

参考资料:方正金工专题报告《凤鸣朝阳:股价日内模式中蕴藏的选股因子》,作者:魏建榕

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约