Quant工具箱:量化开发之向量化回测框架

基于Scikit-learn的向量化回测框架

回测是个老掉牙的问题了,开源社区也有不少优秀的回测框架,如zipline、backtrader等,那我们为什么要放弃他们而选择造轮子再设计一套呢?

其实我们并非放弃这些框架,只是在某些场合下,我们需要回测速度更快、数据处理能力更强的一个框架。我们将用后者快速验证因子或策略逻辑的有效性,然后再用前者进行精细化的回测。简而言之,前者“快”而后者“细”,优势互补,提高研究的效率。

那么为了实现“快”,我们需要做些什么呢?

很简单,感谢Python强大的生态圈,你需要做的最主要的事,是读文档、调包和组合。



文章将分为3个部分,逐一讲解向量化回测的设计原则、架构与实现


设计原则

架构设计方面,有两个原则。

  • 充分利用现有开源库,不造轮子
  1. 业务优先,不过度设计
  2. 主流库历经验证,坑少、也有社区的支持


  • 以sklearn的接口为标准,可将策略视为算法对象
  1. 强大的社区支持,Google资助
  2. 合理的接口设计,丰富的家族库成员,紧贴Python生态,从sklearn-contrib中就可见一斑
  3. 可并行与集群训练,尤其适合数据量要求高的策略
  4. 便于部署,统一线上、线下程序框架


对了,打个预防针先吧:其实你可以认为,我在鼓励你成为一个调包侠,因为这篇文章的后半部分,基本都是在调用各类主流库的接口。但引用前人的话来说:

从人的客观认识规律来讲,你应该先成为一名调包侠。它有助于你抛开细节的枷锁,清晰地为一个现实目标搭建框架。你可以腾出更多的精力集中于解决方案的设计,而不是在基建设施上停滞不前。这些现成的程序包,能够为你快速地搭建起可以提供反馈的实验模型,让你通过实践来启发更多的思路。

“顺风而呼,声非加疾也,而闻者彰”,人的强大所在,就是在于善于利用工具、组合工具。


架构

流程方面,我们遵循机器学习监督式算法的开发流程,将策略视为自变量X和因变量y之间函数映射关系,f(X;\lambda)

这里f代表映射函数,\lambda 代表映射函数中的参数,而监督式学习的任务就是找到恰当的函数f和参数\lambda ,使得该映射尽量符合以下要求

y = f(X;\lambda)

在实践中,为了实现这一目标,基本流程包括以下几个步骤

  1. 明确业务问题: 定下策略的核心逻辑
  2. 明确特征集: 搜集特征集、目标集数据,即 X, y
  3. 基本计量分析: 对于每个特征标签,均进行统计分析,确认数据质量、了解其数据分布及特点
  4. 制作最小价值模型: 做一个简易的可行模型,例如建立线性回归模型,作为对复杂模型的效果对比


以上步骤一般是非自动化的,需要借助分析员的经验,所谓garbage in garbage out,要从源头就保证好数据与特征的质量。

而以下步骤,则是我们框架中要实现自动化的部分

  1. 特征工程
  • 预处理:必要的数据清洗
  • 因子生成:基于原始数据 X 生成新的因子特征 X_n = h_n(X) ,其中 h_n 为因子n的生成函数
  • 因子筛选:在全部因子中筛选出m个首选因子并进行特征融合

2. 策略训练

    • 针对一组特征因子集,提供 N_1 个备选策略,每个策略都有自己的信号生成方式
    • 针对一个备选策略,提供 N_2 个超参组合
    • 针对一个超参组合,提供 N_3 个交叉验证【可选】
    • 每个交叉验证,对应的是一个带有具体超参组合的策略实例,实例在训练集上进行训练,在验证集上测试


3. 结果输出

    • 生成最优策略实例模型
    • 将测试集导入,生成样本外预测结果
    • 根据效果决定是否上线或者重新调整、训练


流程看起来好像很复杂的样子,但是实现起来,却并非如此。



实现

明确了功能需求之后,我们便可以着手设计框架的架构与实现。

分割器

  • 首先,实现一个数据分割器。功能很简单,能将数据分割成开发集测试集即可。其中,开发集用于模型的交叉验证使用,而测试集用于后期最优模型的效果测试。
  • 实现起来也很简单,设计一个类调用sklearn.model_selection.train_test_split函数即可。留心是否要设置shuffle=False,毕竟针对于策略而言大部分数据都是时序数据。
  • 特别要注意的是,测试集数据在最后的模型测试阶段前,无论如何,都不应该被触碰。


因子处理器

因子预处理器以管道Pipeline形式串联或者并联多个Transformer,包括:

  • 预处理方面,主要采用sklearn.preprocesser中的组件,如MaxAbsScaler, Imputer, StandardScaler
  • 因子生成方面,则定制化因子转换器,形如MATransformer这种形式,须实现接口fit, transform以便接入到Pipeline
  • 因子筛选方面,可直接采用sklearn.feature_selection.SelectKBest,或者根据业务设计定制化筛选器


from sklearn.base import TransformerMixin, BaseEstimator
import pandas as pd
class MATransformer(TransformerMixin, BaseEstimator):
    def __init__(self, n_sma):
        super(MATransformer, self).__init__()
        self.n_sma = n_sma

    def fit(self, X, y):
        return self

    def transform(self, X):
        return pd.DataFrame(X).rolling(self.n_sma).mean().values


Transformer的形式来设计因子是颇有裨益的。一来可以将数据处理流程模块化,方便代码管理及复用。二来可使用管道的方式轻松地将不同的数据处理流程连接起来,方便多种因子的组合拼接。

通常而言,为了找到一个好策略,你还需要定义其他因子,形容均线类因子,突破类因子、形态类因子等等,寻找有效因子一直都是研究员最重要的任务之一。抽象来说,因子本质上是从原始数据中抽取出来的特征,隶属于特征工程,是一个很大的话题,所以这期不会深入探讨,这块内容计划在未来的【Analyst-特征工程】一期中做探讨与分享,希望不跳票 :)

策略学习器

这块是对策略实例进行训练和优化,是整个框架的核心部分

流程方面,主要是如下4步:

  1. 针对一组特征因子集,提供 N_1 个备选策略,每个策略都有自己的信号生成方式
  • 遍历 N_1 个备选策略的优化过程,选取最优策略


2. 针对每个备选策略,提供 N_2 个超参组合

    • 对于每个备选策略都使用sklearn.model_selection.GridSearchCV来做暴力网格优化
    • 根据需求,也可以选择随机优化算法,形如模拟退火、粒子群等


3. 针对一个超参组合,提供 N_3 个交叉验证【可选】

    • 如果实现这一步,建议采用在GridSearchCV的参数中设定cv=TimeSeriesSplit()以指定训练集和验证集的切分


4. 每个交叉验证,对应的是一个带有具体超参组合的策略实例,实例在训练集上进行训练,在验证集上测试

    • 策略是如何训练的:fit()函数
    • 策略如何进行信号生成:predict()函数
    • 如何判断策略是优是劣质:score()函数

可以看到,由于每次完整的流程,都至少要经过 N_1 * N_2 * N_3 次策略实例训练,因此对单次训练的速度就有很高要求。显然逐bar进行回测并不是一个好的选择,此时我们应该选择一个更好的数据结构和算法完成策略的训练,作为python准一等公民的numpypandas立刻映入眼帘,特别是pandas.DataFrame,拥有着强大的数据向量化处理能力,接口友好,十分适合作为我们的基础数据单元X进行传递。

因而到此,我们确定下了3个策略基类的重要接口,并以DataFrame形式传递数据

from sklearn.base import BaseEstimator
class Strategy(BaseEstimator):
    def fit(self, X, y):
        pass

    def predict(self, X):
        pass

    def score(self, X, y):
        pass


数据结构方面,对于传统的机器学习数据集,其维度一般是2维的,每列1个标签,每行1个个体数据。然而对于股票这样的时间序列数据而言,情况就稍有一点不同了。

以择股策略为例,数据通常都是三维的,包括【股票,时间,特征】。股票数据序列一般都有自相关性,展开成二维矩阵时,若处理不当,则很容易丢失时间维度上的信息。因此,可以考虑采用三维结构来存储数据信息。一来保留数据的维度信息,二来这样的结构也符合研究员的直觉。

庆幸的是,python包xarray中的类DataArray提供了这方面的功能支持,高效支持向量化运算的同时,也保留了轴和维度元信息,操作同样简单便利,相当于是一个高维度的pandas.DataFrame。所以在fit()接口收到X这样的二维矩阵数据时,不妨先转换成3维数据以便于后期模型逻辑的处理。

而整个策略训练的过程,实际上可以看做我们从3维矩阵中提取标量指标的过程。其过程如下:


  1. 通过对3维矩阵的处理,我们能得到2维的信号矩阵数据,横轴为时间,纵轴为标的名称,值为对应时间下、给定标的的信号值(权重值)
  • 利用xarray.DataArray.sel().to_pandas()的操作,我们能便捷的抽取出某个特征标签的矩阵信息,以策略逻辑叠加处理多个特征信息,获得最终的信号矩阵信息

2. 得到信号矩阵后,我们确认了各个时间点的标的持仓情况,以此,可以计算下个时间阶段的收益情况,进而得到整个资产组合的收益曲线

    • 一般情况下,我们会对信号矩阵采用shift(1)的操作,然后和收益矩阵close_df.pct_change()进行点乘,并按标的轴方向加总,得到净值曲线


3. 对于收益曲线,一般以年化收益、夏普比率、或者最大回撤幅度作为策略拟合优度的度量

最简单的例子只需要四行代码即可。

signal_df = (ma_12 > ma_26).astype(float)
pnl_ratio = (signal_df.shift(1) * close_df.pct_change()).sum(axis=1)
nav = (1 + pnl_ratio).cumprod()
total_return = nav.iloc[-1] - 1


可以注意到,整个过程中的,我们刻意的不使用任何for循环,而是把所有的数据处理任务交由xarray,pandas, numpy,它们是高效的数据处理包,能帮助我们实现充分向量化的目标,到达快速训练和回测的效果。对于单机可处理的数据量而言,数据越大,加速效果越好。

训练完毕后,score(X, y)会以同样的流程处理测试数据,输出模型的样本外训练效果。

到此,我们便完成了一个带有具体超参组合的策略实例的训练,剩下的工作,交给GridSearchCV即可,你可以去冲杯咖啡,回来就该有一个最优策略模型出来了。

评估器

通过策略学习器,我们得到开发集上的最优策略模型,现在需要我们进行最后的收官测试阶段

  1. 先以整个开发集X_dev, y_dev重新训练 带有最优超参的策略模型
  2. 然后以测试数据X_test, y_test(again,测试集数据在此阶段之前,无论如何,都不应该被触碰)输入评估策略效果,如果效果优良,那么恭喜你,你得到了一个统计学上验证有效的策略!

小结

到此为止,“调包侠”的任务全部完成,你会发现回测框架的代码量并不多,大部分代码逻辑都在讲如何把现有库的接口以合理的形式聚集起来。对于研究员而言这已然足够,自动化且高效率的训练验证流程已经可以让他们潜心研究,将精力放在因子和策略的设计上。

当然,这个框架也还有两方面的不足:

  1. 一者,框架目前还是单机运行的,对于简单策略而言是足够的,但是如果策略涉及了复杂的处理逻辑,或者数据量庞大,单机显然是无法处理过来的。土豪的方案一般是扩内存、提性能,而更一般的方案则是集群运行。庆幸的是我们采取了sklearn的设计方案,你不需要重头进行集群设计,你已经拥有了不少现成的且经历验证的集群库,动手尝试一下daskspark-sklearn,集群化也触手可及。
  2. 二者,是策略模型还不能实盘。当然,如果不是中高频策略,对延迟要求不高,我们训练出来的结果,还是有上实盘的能力的。我们需要额外做的是:
  • 定义一个数据抓取器:能按时将特征数据抓全,并整理成DataFrame形式送到我们的策略实例,由策略实例生成信号矩阵
  • 定义一个简单的发单器:能将信号矩阵处理成对应的订单发送给交易所。


但是,此时我们还是不建议直接上实盘的,因为这个系统还太脆弱了,没有风控,也没有仓位管理,这好比是一辆车,装了引擎能跑,却没装刹车停不下来。这样的系统是相当危险,一不小心,就容易翻车。

稳重的你一定不想翻车也不会翻车,所以希望你继续关注我的下篇文章【事件驱动回测系统】,敬请期待啊 :P

编辑于 10-06

文章被以下专栏收录