因子回测可以看做是因子分析的精细加强版。
因子分析可以大概看出因子对收益的预测准确性,稳定性等等指标。不同于分析,回测需要尽量模拟历史的股票行为,不能使用未来数据。比如某一只股票集合竞价阶段就已经涨停,那么在收盘前调仓时是无法买入的,必须在调仓时滤除这部分的股票。同时回测还要考虑到组合构建的问题,精准的回测系统才能更好的体现因子真实的表现。
量化投资研究服务平台因子回测帮助文档中已经提供了用于回测的类:FactorBackTest。
因子回测流程主要包括如下步骤,如图所示: 1. 因子输入 2. 股票过滤 3. 因子合成 4. 因子选择 5. 股票赋权 6. 行业中性调整(可选)
回测的第一步就是因子的输入。这里默认回测的因子已经写入数据库,所以仅仅需要提供因子的名称和方向即可,回测系统会根据这些信息自动从数据库中读取因子,加载到回测系统。
因子的名称和方向需要用python dict的数据结构提供,每个因子一个dict,最终将这些dict放到一个list中,作为参数,传入回测的类。
例子:
factors = [{'name':'MonthlyReturn', 'direction':1},
{'name':'RoeGrowth1', 'direction':-1},
{'name':'MonthlyTradeAmount', 'direction': 1}]
目前支持三个最常用的调仓频率,即
如果没有参考基准,回测的结果就很难评估和比对。目前线上支持多达320多个指数作为基准,但是常见作为基准的指数主要有
一般持仓股票的数量都会有一些限制,数量过多,会平滑收益,很难战胜指数。数量过少,又会暴露更多的风险在个股或者行业上。所以这里也要设定好一个持仓数量。
在回测过程中需要对很多状态的股票进行过滤,比如停牌的股票,涨停的股票,ST的股票。
停牌和涨停的股票是必选过滤器,因为这些股票在回测过程中无法交易。
ST股票是可选的,默认不买,也可以通过接口设为可买。另外还有是否买入跌停的过滤器也是可选的,详细的文档请看API文档
经过上一步的过滤后,现在的因子都是可交易的了,但是因为是多个因子,这里还需要进行合成。合成一个因子后才会进行回测。
用户也可以用自己的合成算法,将合成后的因子直接存入数据库,然后将这个数据库中的因子传入回测类,就会跳过这个环节。
这里有两个细节: 1. 合成前会对每个因子做归一化,然后在等权相加后处以均值。 2. 如果任何一个股票的因子值为NULL,那么合成之后的结果也是NULL,最终会被drop掉
因子合成后,会有很多的股票,这些股票肯定不能都用于交易。这时我们要选取部分股票,剔除其他的股票。选股的方法有很多种,比如按比例选股,选择因子排名最靠前的股票等等选择方法。
目前回测框架中支持按因子排名选择最靠前的N个,N是用户自己可以选择的。
股票选择完成后,我们已经得到持仓了。但是每个股票的权重到底应该怎么分配呢?目前回测平台支持等权的方式和rank分层的方式。后面会加入市值加权的方法。
rank分层的方法流程:首先将因子值进行rank,然后按序分层N等分,每一等分的权重是相同的。这种方式可以将因子值靠前的赋予更多的权重,同时避免了权重分配的“过拟合”。
在回测中会默认进行行业中性调整,行业会将股票的权重按照基准中的行业的权重进行调整,使其贴近于基准中的行业配比。这个功能可以通过方法屏蔽掉。
在行业中性调整时,会遇到股票在某些行业中是缺失的,回测系统默认也会进行行业填充,保证持仓行业权重与基准的行业权重完全match上。
回测框架支持单行业回测,包括申万一二三级的行业。因为在很多因子在特定的行业会表现比较好,所以对因子进行单行业回测也是非常有帮助的。可以帮助发现在某个行业中,哪些因子表现比较好。
交易总会产生费用,越是频率高的策略产生的交易费用越高。过高的交易费用会显著吞噬掉收益。如果回测中不加入交易费率的设置,肯定会影响策略的表现,尤其是日频的策略。
回测框架目前提供了接口,可以灵活的设置买入费率,也可以设置卖出费率。
[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
如果用户有更灵活的自定义需求,可以自己写函数,然后传入回测类中,设置好先关的参数即可。
用户自己写函数只需要满足两个条件:
[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
目前回测功能主要由FactorBackTest这个类来实现。回测中需要多种参数的设置,除了类本身的几个参数(如下),其他个性化参数设置都通过接口进行
FactorBackTest需要显式初始化下面的参数:
[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')
设置选股域,参数为’A’时(默认值),从全A股中选取股票。也可以设置为指数代码,例如’000905.SH’,代表从中证500中选取股票
[4]:
bt.set_select_scope('A') # 从全A股中选取
# bt.set_select_scope('000905.SH') # 从中证500中选取股票
是否买入ST股票,默认为False,不买入。
[5]:
bt.set_buy_st(False) # 不买被st的股票
是否买入跌停的股票,默认为True,即:股票跌停也可以买入
[6]:
bt.set_buy_limit_down(True) # 如果为True,则表示买入跌停股票, 默认为True
设置交易费用,默认买入费率为0, 卖出费率为0。
[7]:
bt.set_commission(buy_cost=0.0005, sell_cost=0.0015)
设置是否为行业中性,默认为True,即等权的构建方法也需要进行行业中性。如果设置了6.2中的单行业回测,则这个参数置为False。因为默认输入的因子是单个行业的因子。
[8]:
bt.set_industry_neutralization(True)
Attention:这个设置会开启单行业回测的功能,目前仅支持申万一级行业
如果进行下面的设置,会将因子中不属于汽车的股票全部剔除,并且不会进行行业中性化和行业填充。回测结果中的基准收益曲线就是基准中(例如中证500)汽车行业的收益曲线。
[9]:
# bt.set_industry_v1_code('SW801880') # 汽车行业
回测执行函数,无需传入函数
[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
因子加载完成
CPU times: user 52.1 s, sys: 5.2 s, total: 57.3 s
Wall time: 1min 4s
将组合累积收益、基准累积收益,alpha曲线plot出来
[11]:
bt.analy_plot()
获取回测期间的交易日列表
[12]:
bt.get_trading_days()[:5] # 仅仅展示前5个
[12]:
['20150105', '20150106', '20150107', '20150108', '20150109']
获取调仓日期列表
[13]:
bt.get_adj_days()[:5] # 仅仅展示前5个
[13]:
['20150130', '20150227', '20150331', '20150430', '20150529']
获取组合收益序列,类型为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
获取组合累积收益(累加), 类型为Series,index为datetime类型的时间。
[15]:
bt.get_portfolio_cum_ret().plot(grid=True)
[15]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f19a8365c18>
获取基准收益序列,类型为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
获取基准累积收益(累加)
[17]:
bt.get_benchmark_cum_ret().plot(grid=True)
[17]:
<matplotlib.axes._subplots.AxesSubplot at 0x7f1989668c88>
获取每个交易日的持仓信息,参数为字符串类型的日期。如果没有传入参数,则返回每个交易日的持仓,这个时候的数据量会比较大
[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
获取某一个调仓日调仓前的持仓信息
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 |
获取调仓前后权重变动比例
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 |
该函数类似于run这个接口,会加载本地调仓文件中的信息进行回测。
FactorBackTest类中仅仅benchmark参数有效,其他参数都会根据文件中的信息进行重置
开始日期会自动设为第一个调仓日,结束日期为最后一个调仓日.
param file_path: 调仓记录的csv文件路径,必须要包含securityId,dateTime, securityWeight三列
[ ]:
bt.run_port_list('adb_all20161031_20170731_000905H_AI_Ada_nSTSus_200_kFin.csv')
根据组合收益和基准收益,输出常见指标,如最大回测,每日收益等等
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]
微信扫码分享本页