Skip to content

yuba316/FactorBackTest

Repository files navigation

FactorBackTest

my first factor-stock-selecting backtest function

2020/08/15 09:53(第五次更新)

在因子挖掘上我可以说一直都是小白,有的经验只是知道哪些已经测试过的因子是无效的,我更想形成一套非常有逻辑、体系化一点的因子开发流程(比如说能够通过回测结果很快的意识到这个因子存在的问题在哪?表达式这么写它的收益还是换手应该是怎么样的?实际又是怎么样的?该通过给表达式加入怎么样的公式可以改善这种问题?),我希望对一些常见的疑惑能有一个更加清晰的认识,并且能越来越快地去解决遇到的问题。
这次更新的是这周一直在写的基于gplearn第三方库的遗传规划挖掘量价因子的python代码。但是我可以说,这个程序非常无用,几乎是没有完成,因为我始终无法解决一个问题。
相较于上一次上传,这次我全部改写了算子使它们能够对多只股票进行同时计算(其实也就是用了groupby的方法,跟之前提交的FactorCalculator.py文件里的函数几乎完全一样)。除了上次就发现的常数无法作为特殊的参数传入自定义的函数以外,一个在算法层面上的问题一直困扰着我。
我们输入的训练集特征是dataframe([close,open,high,low,pre_close,……])这一类行情数据,而我选择作为标签的是未来20天的收益率,但很明显,特征和标签之间并不具有相关性!也就是说,谁告诉你今天股票的close、open、high、low长这样,20天后它的收益率就一定是那样的,但是算法就是按照这个逻辑去训练了,去预测了20天后的收益率。如果这个时候,你用你预测的收益率和原本的标签的相关系数作为适应度的话,你训练出来的结果,极有可能就是close、open……本身(因为我根本不用进行任何算子的运用啊我直接用close预测得到的就是训练集里的标签啊一模一样!!!)……
所以我觉得这个库最局限的地方在于它允许用户传入自定义的适应度函数里的参数,只有标签和预测结果这两个,而我更想要的是每一代种群的计算结果,也就是因子得分,而不是你基于这个因子得分给我瞎预测出来的收益率!
所以问题就在这,我要如何自定义一个非常好的自适应函数来训练我的种群(在这个体系下似乎是行不通的,因为只有标签和预测结果两个值可以使用,所以我打算周末换Deap库来看看)?其二是我换一种标签来使得它在当前体系下是可以运算出较为有逻辑的结果的(我试过把标签改成close、open……每一天的RankIC值,但感觉还是有哪里非常不对劲……归根结底还是机器根本不知道我做了groupby这一步操作,所以……如果我把groupby也作为一个算子呢……我去!好像有点道理!下午来试试!不打穿越火线了……诶……好难)
p.s.:新更新的文件为:Factor_gplearn_2.py

2020/07/28 11:10(第四次更新)

时隔一个多月,我终于落实了去实习的事情!本来是不打算再继续研究这个量价因子挖掘器的,但是面试的时候了解到公司好像还是以做股票为主,可能需要我处理高频因子之类的事情,所以就想来了解一下。结果被我发现了一篇用python的gplearn库实现遗传规划挖掘因子的研究,遂复刻了下来,同时对它里面用到的自定义函数进行了修正与扩充。

先简单地讲讲遗传规划挖掘因子的过程吧,我们把close、open、volume等这些个股的行情数据和常数都视作是可以输入到某个计算函数里的变量(比如说plus(close,10)输出的就是close+10)。gplearn实现的就是随机地将你给定的这些变量代入到已有的计算函数库里去生成公式(也就是我们所要的成品——因子),再根据你给定好的适应度(也就是我们所熟悉的目标函数)来筛选出较好的公式来遗传给下一代去交叉和变异,最终输出适应度最好的公式(差不多是这个意思,感兴趣的话可以去看看下方参考文献中的资料,主要是华泰的几篇研报)。

所以在这里,自定义的计算函数就很重要了,因子肯定不会是只用加减乘除就能凑出来的啊,我复刻的研究就多给了一些与时间序列相关的算子。但是gplearn的自定义函数似乎有个问题,我无法设定输入的变量是close一类的行情还是常数(毕竟像SMA这样的算子肯定得有一个输入参数是常数)。这就很麻烦了,原文的作者干脆给的都是些一元的计算符,即把SMA这些函数的计算周期都写死了,全部设置成10……我看华泰的那篇文章,虽然也有这些时间序列类的算子,但是输出的因子里面带时间周期的几乎都是些整5整10的数字,所以我猜他们可能也是把5、10、15……这些常用的周期作为和close一样的变量输入到gplearn里面去了。虽然我很想有反转,我也在csdn和github上面翻了很多人的代码,但是都没有找到怎么改变gplearn这个自定义函数设定的办法(他们的API也写得很少很少……),所以我妥协了,就只用了5、10、15、20、30、60、120、200这几个数……

这个项目有缺陷吗?有!非常多!首先是因为我不能控制输入的是行情还是常数,就很有可能出现SMA(close,open)这样奇怪的公式。我还在原函数里加了个判断条件,如果type()!=int就return np.zeros(),结果还是会有这种奇怪的公式出来我这我……还有就是这篇文章给的方法只能用在单只股票上,就是说输入的数据集是一个np矩阵,列为close一类的行情,行为日期……不行,这样真的不行,我希望它输入的是一整个股票池,我自己上次写函数的时候都是设计给一整个股票池使用的,所以会用到groupby。这就导致了自定义函数和适应度,即目标函数的设计都很不一样。我这里的适应度给的是隔日open到close的收益率的累加值,如果换成是一整个股票池的话应该是因子的IC值或者多空收益之类的。

当然也有改进的一些地方,除了上面提到的我把原文给的一元算子都强行改成了二元的以外,我还加入了相关系数、回归残差这一类的三元算子(X、Y和周期)。总之这个项目只能说是让你初步地入门一下遗传规划挖掘因子的过程,我觉得领入门还是挺重要的,虽然它结果很差,但至少还是有结果的嘛,这就是一种激励,未来我会尝试着去把它改写成适用于股票池的因子挖掘器。不过话说回来,要去实习了,不知道什么时候会有空钻研……当然是每天晚上和每个周末啦!

遗传规划挖掘因子结果

gplearn因子

代码需要自己去注册聚宽的账号领取免费的jqdatasdk试用一年,安装jqdatasdk和gplearn包(都可以用pip install完成)。

参考文献:

2020/06/21 12:36(第三次更新)

这次上传的是适用于DataFrame格式的股票数据库的众多计算函数。我们之前拿来做回测的因子得分数据都是DataFrame格式的,索引为由(trade_date,ts_code)组成的元组,所以我们计算因子得分时所依赖的数据也是DataFrame格式的,且也有trade_date和ts_code两列才能定位到单个数据元素(即某天某只股票的open、close等数据)。计算函数包括普通的加减乘除、取对数指数……时间序列上的排序、取大取小、标准化、移动平均、相关系数……以及横截面上的排序和标准化等等。在这期间确实学到了如何灵活地运用GroupBy和Rolling,唯一不能实现的是分组取指数移动平均(大概是因为它涉及到从头累加的关系,总之.ewm()不适用于.groupby()),比较可惜……事实上我写这么多函数是为了方便后期能够实现自动化的因子挖掘过程,因为之前也看到蛮多研报有在说他们都是用机器来挖掘短期量价因子的,所以我也希望之后能够实现这一功能,看看去实习前有没有时间来完成它吧哈哈哈……感觉是项大工程……

2020/06/16 10:57(第二次更新)

上一次更新提到的那种回测方法其实非常地不科学(准确地来讲应该是麻烦费时且没必要,我在写的过程中也怀疑了好几次,但总以为那么写是最严谨的,所以费了很多时间,程序跑起来也很慢,因为有很多现在看来是可以省去的循环),所以这次改用一种更加普遍的方法做回测: 每隔一段时间(1周或1个月)依照因子得分设置权重并进行投资。不论当前股票是否出现在上一轮投资中,都先平仓,然后再按新的因子得分对其进行买卖。 事实上这么做以后,代码量变少了许多,实现起来也很简单,但是为了画出每日的策略回测收益图,for循环还是逃不开,所以速度还是有点慢的(这一函数放在了BackTest_Factor.py的FactorBT_2函数里)。但实际上我们还可以再快很多,因为受到第一次更新中复杂的代码实现内容的影响,我的思路被限制住了,没有想到可以巧妙地运用groupby.apply函数来实现相同的回测功能,且可以提速很多。具体的实现方法我放在了新的BackTest_Factor_2.py文件里了。Factor_Test.py是单纯用来计算因子得分的(计算IC、RankIC、IC_IR等指标,其实还可以包含之前的回测结果去计算夏普比率之类的指标,只要回测函数的实现过程够快),因为之后想尝试复刻遗传规划等方法自动地挖掘短周期量价因子,所以就先构造了评分体系,但是速度还是不够快,后期先换vaxe库,把文件输入类型从csv换成hdf5,试着提高数据处理的速度,满足机器能够在短时间内尽可能多地测试因子。下方的回测和因子测试结果和之前的都是同一个因子:

回测结果

回测结果

因子测试结果

因子测试结果

2020/06/06 22:40(初次更新)

时间过得也太快了吧,一转眼就6月份了,5月回了趟老家后就啥事也没干,3、4月都还有每个月坚持看一本交易相关的书,5月彻底放弃了hhh。赶在6月出头把这个因子选股的回测函数给写了,下星期开始继续看书了。这个回测函数可以说不是特别的完善,我并没有考虑手续费之类的其他费用,而且允许空头,但具体的操作又不太一样,我会在下面做详细的阐述。

首先我希望这个回测函数实现这样的一个功能,每天按照各只股票的因子得分进行排序,选取头尾给定参数百分比的多只股票组成多空组合,可以选择等权,也可以选择按照排名先后加权购买相应的股票数。这里需要注意的是,等权是指两只股票购入的份数一样而不是金额一样,所以你需要考虑不同个股间的价格差。具体计算公式是:

weight × Capital/(sum(close×weight))

当然这一点也给我们在计算每日收益时带来了不便,你需要考虑原有的股票在今天的权重变化,如果权重变小了就需要对其进行平仓,反之加仓,我们应该先平仓才能保证有充足的资金再开仓。但由于价格每天都在变动,而我们的权重是相对于当日横截面上所有入选股票而言而不是和自身昨天的权重去做对比,所以简单地从自身的权重放大或缩小来判断加减仓并不严谨,还是需要引入价格来判断:

平仓 if sum(last_close×last_weight)/sum(close×weight) × weight < last_weight

加仓的比例也同样要这么计算。接下来是每日收益的计算,事实上这一点我在写期权回测函数的时候就已经提到过了,由于是买入并持有,所以收益的计算并不是每日收益的累加,而在因子选股里,我们可以把每日的总资产分为三个部分:

每日总资产 = 股票资产+股票收益+买股票剩下的钱

  • 股票资产 = sum(持有股票数 × 开仓时价格)
  • 股票收益 = sum(持有股票数 × 开仓至今收益)
  • 买股票剩下的钱 = 上次剩下的钱+今天平仓收到的钱-今天开仓花掉的钱

由于几乎每天都会有开平仓,所以每天都要重新计算当前所持有的股票仓位,对于原有的仓位,其开仓价格则保持不变,而新增的仓位,开仓时价格就是今日收盘价。我们一般会在开仓后第二天一开始计算新的每日总资产,因为这样做会比较方便,算完之后再来计算第二天的新股票资产和剩下的钱,留给第三天计算用。

最后我随意地测试了一个因子:close/pre_close-1,也就是根据昨天的每日收益率作为因子得分来确定今天的股票开仓权重,回测结果如下(只是随意测试的一个因子,并不用在意收益的好坏):

每日收益因子选股策略回测结果

因子选股策略回测结果

最后再给大家分享一个DataFrame数据处理的小技巧:
比如说你有一个很大的表,[trade_date,stock_code]可以确定一只股票在某一天的行情数据。现在你想算每一只股票的月度收益率,你可能会想用简单的df[close]/df[close].shift(21)-1来处理。但是你发现,不论数据是sort_values by trade_date还是stock_code都不太对劲,因为不同的股票是连在一起的,在切换股票的时候,shift(21)总会牵扯到上一只股票的数据,这就需要用到group by了。

dfgb = df.groupby(['ts_code'])
df['last_month_close'] = dfgb['close'].apply(lambda x: x.shift(21))
df.dropna(axis=0,inplace=True)
df['score'] = df['close']/df['last_month_close']-1

巧用group by.apply(lambda x: )就可以轻松地实现你想要的效果啦~

About

my first factor-stock-selecting backtest function

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages