因子回测帮助文档

因子回测可以看做是因子分析的精细加强版

因子分析可以大概看出因子对收益的预测准确性,稳定性等等指标。不同于分析,回测需要尽量模拟历史的股票行为,不能使用未来数据。比如某一只股票集合竞价阶段就已经涨停,那么在收盘前调仓时是无法买入的,必须在调仓时滤除这部分的股票。同时回测还要考虑到组合构建的问题,精准的回测系统才能更好的体现因子真实的表现。

量化投资研究服务平台因子回测帮助文档中已经提供了用于回测的类:FactorBackTest

因子回测流程概览:

因子回测流程主要包括如下步骤,如图所示: 1. 因子输入 2. 股票过滤 3. 因子合成 4. 因子选择 5. 股票赋权 6. 行业中性调整(可选)

image0

1、回测必选参数

1.1 因子输入

回测的第一步就是因子的输入。这里默认回测的因子已经写入数据库,所以仅仅需要提供因子的名称和方向即可,回测系统会根据这些信息自动从数据库中读取因子,加载到回测系统。

因子的名称和方向需要用python dict的数据结构提供,每个因子一个dict,最终将这些dict放到一个list中,作为参数,传入回测的类。

例子:

factors = [{'name':'MonthlyReturn', 'direction':1},
            {'name':'RoeGrowth1', 'direction':-1},
            {'name':'MonthlyTradeAmount', 'direction': 1}]

1.2 调仓频率

目前支持三个最常用的调仓频率,即

  • month_end: 每个月最后一个交易日调仓
  • week_end:每周最后一个交易日调仓
  • daily: 每个交易日调仓

1.3 参考基准

如果没有参考基准,回测的结果就很难评估和比对。目前线上支持多达320多个指数作为基准,但是常见作为基准的指数主要有

  • 中证500
  • 沪深300
  • 上证50

1.4 持仓数量

一般持仓股票的数量都会有一些限制,数量过多,会平滑收益,很难战胜指数。数量过少,又会暴露更多的风险在个股或者行业上。所以这里也要设定好一个持仓数量。

2、过滤器

在回测过程中需要对很多状态的股票进行过滤,比如停牌的股票,涨停的股票,ST的股票。

停牌和涨停的股票是必选过滤器,因为这些股票在回测过程中无法交易。

ST股票是可选的,默认不买,也可以通过接口设为可买。另外还有是否买入跌停的过滤器也是可选的,详细的文档请看API文档

3、因子合成

经过上一步的过滤后,现在的因子都是可交易的了,但是因为是多个因子,这里还需要进行合成。合成一个因子后才会进行回测。

用户也可以用自己的合成算法,将合成后的因子直接存入数据库,然后将这个数据库中的因子传入回测类,就会跳过这个环节。

这里有两个细节: 1. 合成前会对每个因子做归一化,然后在等权相加后处以均值。 2. 如果任何一个股票的因子值为NULL,那么合成之后的结果也是NULL,最终会被drop掉

4、选股

因子合成后,会有很多的股票,这些股票肯定不能都用于交易。这时我们要选取部分股票,剔除其他的股票。选股的方法有很多种,比如按比例选股,选择因子排名最靠前的股票等等选择方法。

目前回测框架中支持按因子排名选择最靠前的N个,N是用户自己可以选择的。

5、股票赋权

股票选择完成后,我们已经得到持仓了。但是每个股票的权重到底应该怎么分配呢?目前回测平台支持等权的方式和rank分层的方式。后面会加入市值加权的方法。

rank分层的方法流程:首先将因子值进行rank,然后按序分层N等分,每一等分的权重是相同的。这种方式可以将因子值靠前的赋予更多的权重,同时避免了权重分配的“过拟合”。

6、行业中性调整

在回测中会默认进行行业中性调整,行业会将股票的权重按照基准中的行业的权重进行调整,使其贴近于基准中的行业配比。这个功能可以通过方法屏蔽掉。

在行业中性调整时,会遇到股票在某些行业中是缺失的,回测系统默认也会进行行业填充,保证持仓行业权重与基准的行业权重完全match上。

7、单行业回测

回测框架支持单行业回测,包括申万一二三级的行业。因为在很多因子在特定的行业会表现比较好,所以对因子进行单行业回测也是非常有帮助的。可以帮助发现在某个行业中,哪些因子表现比较好。

  1. 当用户设置行业代码后,回测系统将不在该行业的股票滤除,仅仅在该行业中进行股票的选取。
  2. 此时的基准收益将会是设置的基准里面的该行业的历史收益。

8、交易费用

交易总会产生费用,越是频率高的策略产生的交易费用越高。过高的交易费用会显著吞噬掉收益。如果回测中不加入交易费率的设置,肯定会影响策略的表现,尤其是日频的策略。

回测框架目前提供了接口,可以灵活的设置买入费率,也可以设置卖出费率。

[1]:
%matplotlib inline
import pandas as pd
import datetime
import time

from data_provider.datafeed.universe import Universe
from data_provider.nestlib.trading_cal import TradeCal
from data_provider.datafeed.quote_feed import QuoteFeed
from data_provider.nestlib.market_info import Frequency
from smartbeta.factorbase import BaseFactor
from smartbeta.smartfactor import SmartFactor
from data_provider.nestlib.progress_bar import ProgressBar
import numpy as np
import pdb
from smartbeta.backtest.factor_backtest import FactorBackTest

9、自定义信号回测

如果用户有更灵活的自定义需求,可以自己写函数,然后传入回测类中,设置好先关的参数即可。

用户自己写函数只需要满足两个条件:

  • 输入参数为字符串类型的日期,如:‘20170718’
  • 输出的是当天的信号,类型为pd.Series类型,index为股票代码,value为信号值
[2]:
def get_big_mkt(trading_day):
    mkt_df = Universe().get_market_value(trading_day, trading_day)
    mean_mk = mkt_df['total_market_value'].mean()
    return mkt_df[mkt_df['total_market_value'] > mean_mk].set_index('ticker')['total_market_value']

def big_mkt_pe_2(trading_day):
    """大盘股,市盈率最低的10%"""
    big_mkt = get_big_mkt(trading_day)  # 获取当天属于大市值的股票
    pe_se = Universe().get_period_factor_data("ep_ttm", begin_day=trading_day, end_day=trading_day) # 获取这一天的PE因子数据
    pe_se = pe_se.set_index('security_code')['factor_value']
    pe_se = pe_se[pe_se.index.isin(big_mkt.index)]
    pe_se = pe_se[pe_se >= pe_se.quantile(0.9)]
    return pe_se


start = "20170101"
end = "20171205"

bt = FactorBackTest(
    "000905.SH",
    start,
    end,
    factors=None,
    holding_count=None,
    freq="month_end",
    construction_method="equal_weight",
)

bt.set_fun_obj(big_mkt_pe_2)  # 将函数传入即可
bt.set_buy_st(False)  # 不买被st的股票
bt.set_industry_neutralization(False)  # 不进行行业中性调整

bt.run()
bt.analy_plot()
loading dailyreturn time cost 2.03s

100.0%
加载完毕,用时 8.36秒
_images/多因子回测帮助文档_17_8.png

10、接口详细说明

目前回测功能主要由FactorBackTest这个类来实现。回测中需要多种参数的设置,除了类本身的几个参数(如下),其他个性化参数设置都通过接口进行

1. 类的参数说明

FactorBackTest需要显式初始化下面的参数:

  • begin_day:开始日期,string类型,如“20150101”
  • end_day:结束如期
  • benchmark:参考基准,以为指数代码,如’000905.SH‘(中证500)
  • factors:因子名与方向的字典
  • holding_count: 持仓数量
  • freq: 调仓频率 ’month_end’为按月调仓,’week_end’为按周调仓,’daily’为按日调仓
  • construction_method: 构建方法,默认为因子rank后行业中性调整,还可设置为等权’equal_weight’
[3]:
start = '20150101'
end = '20170919'
factors = [{'name':'MonthlyReturn', 'direction':1},
            {'name':'RoeGrowth1', 'direction':-1},
            {'name':'MonthlyTradeAmount', 'direction': 1}]
bt = FactorBackTest('000905.SH',start,end,factors,holding_count=200,freq='month_end')

2. set_select_scope

设置选股域,参数为’A’时(默认值),从全A股中选取股票。也可以设置为指数代码,例如’000905.SH’,代表从中证500中选取股票

[4]:
bt.set_select_scope('A') # 从全A股中选取
# bt.set_select_scope('000905.SH')  # 从中证500中选取股票

3. set_buy_st

是否买入ST股票,默认为False,不买入。

[5]:
bt.set_buy_st(False)  # 不买被st的股票

4. set_buy_limit_down

是否买入跌停的股票,默认为True,即:股票跌停也可以买入

[6]:
bt.set_buy_limit_down(True) # 如果为True,则表示买入跌停股票, 默认为True

5. set_commission

设置交易费用,默认买入费率为0, 卖出费率为0。

[7]:
bt.set_commission(buy_cost=0.0005, sell_cost=0.0015)

6. set_industry_neutralization

设置是否为行业中性,默认为True,即等权的构建方法也需要进行行业中性。如果设置了6.2中的单行业回测,则这个参数置为False。因为默认输入的因子是单个行业的因子。

[8]:
bt.set_industry_neutralization(True)

7. set_industry_v1_code

Attention:这个设置会开启单行业回测的功能,目前仅支持申万一级行业

如果进行下面的设置,会将因子中不属于汽车的股票全部剔除,并且不会进行行业中性化和行业填充。回测结果中的基准收益曲线就是基准中(例如中证500)汽车行业的收益曲线。

[9]:
# bt.set_industry_v1_code('SW801880')  # 汽车行业

7. run

回测执行函数,无需传入函数

[10]:
%time bt.run()
loading dailyreturn time cost 3.42s
加载中: MonthlyReturn
loading monthlyreturn time cost 3.10s
加载中: RoeGrowth1
loading roegrowth1 time cost 4.35s
加载中: MonthlyTradeAmount
loading monthlytradeamount time cost 3.24s
因子加载完成

100.0%
加载完毕,用时 45.38秒
CPU times: user 52.1 s, sys: 5.2 s, total: 57.3 s
Wall time: 1min 4s

8.analy_plot

将组合累积收益、基准累积收益,alpha曲线plot出来

[11]:
bt.analy_plot()
_images/多因子回测帮助文档_35_0.png

9.get_trading_days

获取回测期间的交易日列表

[12]:
bt.get_trading_days()[:5]  # 仅仅展示前5个
[12]:
['20150105', '20150106', '20150107', '20150108', '20150109']

10. get_adj_days

获取调仓日期列表

[13]:
bt.get_adj_days()[:5]  # 仅仅展示前5个
[13]:
['20150130', '20150227', '20150331', '20150430', '20150529']

11. get_portfolio_ret

获取组合收益序列,类型为Series,index为datetime类型的时间。 1. 如果设置了交易费率,会在调仓日减去交易费用。 2. 如果尚未建仓,则组合收益设为benchmark的收益值。

[14]:
bt.get_portfolio_ret()[:3]
[14]:
2015-01-05         NaN
2015-01-06    0.011602
2015-01-07    0.001529
dtype: float64

12. get_portfolio_cum_ret

获取组合累积收益(累加), 类型为Series,index为datetime类型的时间。

[15]:
bt.get_portfolio_cum_ret().plot(grid=True)
[15]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f19a8365c18>
_images/多因子回测帮助文档_43_1.png

13. get_benchmark_ret

获取基准收益序列,类型为Series,index为datetime类型的时间。

[16]:
bt.get_benchmark_ret()[:3]
[16]:
datetime
2015-01-05         NaN
2015-01-06    0.011602
2015-01-07    0.001529
Name: close, dtype: float64

14.get_benchmark_cum_ret

获取基准累积收益(累加)

[17]:
bt.get_benchmark_cum_ret().plot(grid=True)
[17]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f1989668c88>
_images/多因子回测帮助文档_47_1.png

15.get_holding

获取每个交易日的持仓信息,参数为字符串类型的日期。如果没有传入参数,则返回每个交易日的持仓,这个时候的数据量会比较大

[18]:
bt.get_holding('20150204').head()
[18]:
603609.SH    0.00595
000048.SZ    0.00595
000702.SZ    0.00595
002458.SZ    0.00595
300106.SZ    0.00595
dtype: float64

16.get_adj_holdings

获取某一个调仓日调仓前的持仓信息

param date_str: 字符串类型的日期,如果日期为NULL值,则返回相关的统计信息,此时下面的数字均代表各个状态的数量: ‘stock_counts’, ‘abnormal_stock_counts’, ’filled_stocks’分别代表持仓股票数量,持仓中非正常状态(停牌,涨跌停)的股票数量,持仓中不是选股得到,而是行业中性填充的股票数量

param pre_adj: 是否返回调仓前的持仓数据,默认为False,即返回调仓后的数据

[19]:
bt.get_adj_holdings()
stock_counts abnormal_stock_counts filled_stocks
date
20150130 207 0 7
20150227 226 10 18
20150331 253 23 32
20150430 250 32 21
20150529 245 34 12
20150630 239 33 9
20150731 267 58 15
20150831 270 87 13
20150930 232 25 9
20151030 246 27 20
20151130 283 27 61
20151231 242 23 20
20160129 236 22 15
20160229 283 108 21
20160331 268 28 42
20160429 240 31 9
20160531 227 28 0
20160630 261 27 35
20160729 240 22 18
20160831 213 11 2
20160930 222 9 14
20161031 235 4 32
20161130 220 10 10
20161230 218 11 7
20170126 221 7 14
20170228 226 12 14
20170331 222 12 10
20170428 231 19 13
20170531 221 21 0
20170630 237 19 19
20170731 241 18 25
20170831 259 15 44
[20]:
bt.get_adj_holdings('20150529',pre_adj=False).head()
[20]:
weight status filled_stocks
date ticker
20150529 000004.SZ 0.006431 Normal Normal
000019.SZ 0.002055 Normal Normal
000020.SZ 0.007343 Normal Normal
000023.SZ 0.004888 Normal Normal
000032.SZ 0.004711 Normal Normal

17. get_weight_changes

获取调仓前后权重变动比例

param day_str: 字符串类型的日期
[21]:
bt.get_weight_changes('20150731').head()
date ticker pre_adj_weight:% adj_weight:% relative_rate:% status
0 20150731 000004.SZ NaN 0.5496 NaN Normal
1 20150731 000010.SZ 0.3471 NaN NaN Normal
2 20150731 000018.SZ 0.3471 0.5222 50.4641 Normal
3 20150731 000019.SZ 1.8736 0.1934 -89.6776 Normal
4 20150731 000020.SZ NaN 0.3163 NaN Normal

18.run_port_list

该函数类似于run这个接口,会加载本地调仓文件中的信息进行回测。

FactorBackTest类中仅仅benchmark参数有效,其他参数都会根据文件中的信息进行重置

开始日期会自动设为第一个调仓日,结束日期为最后一个调仓日.

param file_path: 调仓记录的csv文件路径,必须要包含securityId,dateTime, securityWeight三列

[ ]:
bt.run_port_list('adb_all20161031_20170731_000905H_AI_Ada_nSTSus_200_kFin.csv')

19. 收益分析

根据组合收益和基准收益,输出常见指标,如最大回测,每日收益等等

param port_return: 组合收益序列,非累积收益,pd.Series类型,index为datetime类型的日期。

param benchmark_return: 基准收益序列,非累积收益,pd.Series类型,index为datetime类型的日期。

这里面仅仅需要传递组合的收益序列和基准的收益序列即可(bt.get_portfolio_ret()和bt.get_benchmark_ret())

[23]:
from smartbeta.analyst import ReturnAnalyzer
ReturnAnalyzer.analyze_return(bt.get_portfolio_ret(), bt.get_benchmark_ret())
整体数据起始日期: 2015-01-05
整体数据结束日期: 2017-09-19


历史回测月份: 31
Performance statistics Backtest
annual_return 0.43
cum_returns_final 1.56
annual_volatility 0.37
sharpe_ratio 1.16
calmar_ratio 0.91
stability_of_timeseries 0.61
max_drawdown -0.47
omega_ratio 1.25
sortino_ratio 1.56
skew -0.89
kurtosis 3.10
tail_ratio 0.91
common_sense_ratio 1.31
information_ratio 0.20
alpha 0.28
beta 1.06
Worst drawdown periods net drawdown in % peak date valley date recovery date duration
0 46.99 2015-06-12 2015-09-15 2016-08-29 317
1 22.85 2016-11-22 2017-06-01 NaT NaN
2 5.50 2015-05-27 2015-05-28 2015-06-01 4
3 4.56 2015-05-04 2015-05-07 2015-05-11 6
4 4.50 2015-04-14 2015-04-20 2015-04-22 7
[-0.045 -0.104]
_images/多因子回测帮助文档_60_4.png

微信扫码分享本页