作者: 阿布
阿布量化版权所有 未经允许 禁止转载
abu量化系统github地址 (您的star是我的动力!)
上一节主要讲解了如何使用abupy中度量模块对回测的结果进行度量,本节将主要讲解使用grid search模块对策略参数寻找最优。
最优参数的意思是比如上一节使用的卖出因子组合使用:
sell_factors = [
{'stop_loss_n': 1.0, 'stop_win_n': 3.0,
'class': AbuFactorAtrNStop},
{'class': AbuFactorPreAtrNStop, 'pre_atr_n': 1.5},
{'class': AbuFactorCloseAtrNStop, 'close_atr_n': 1.5}
]
AbuFactorAtrNStop止盈止损策略的止盈阀值是3.0,止损阀值是1.0,那么止盈参数设置成2.0或者止损阀值设置为0.5是不是回测交易效果更好呢, AbuFactorPreAtrNStop风险控制止损pre_atr_n参数设置的是1.5,如果设置为1.0回测结果将如何,grid search模块对策略寻找最优参数就是要在给定的参数组合范畴内,寻找到策略参数最佳的设置。
首先导入abupy中本节使用的模块:
In [1]:
# 基础库导入
from __future__ import print_function
from __future__ import division
import warnings
warnings.filterwarnings('ignore')
warnings.simplefilter('ignore')
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
import ipywidgets
import os
import sys
# 使用insert 0即只使用github,避免交叉使用了pip安装的abupy,导致的版本不一致问题
sys.path.insert(0, os.path.abspath('../'))
import abupy
# 使用沙盒数据,目的是和书中一样的数据环境
abupy.env.enable_example_env_ipython()
In [2]:
from abupy import AbuFactorAtrNStop, AbuFactorPreAtrNStop, AbuFactorCloseAtrNStop, AbuFactorBuyBreak
from abupy import abu, ABuFileUtil, ABuGridHelper, GridSearch, AbuBlockProgress, ABuProgress
stop_win_range = np.arange(2.0, 4.5, 0.5)
stop_loss_range = np.arange(0.5, 2, 0.5)
sell_atr_nstop_factor_grid = {
'class': [AbuFactorAtrNStop],
'stop_loss_n' : stop_loss_range,
'stop_win_n' : stop_win_range
}
print('AbuFactorAtrNStop止盈参数stop_win_n设置范围:{}'.format(stop_win_range))
print('AbuFactorAtrNStop止损参数stop_loss_n设置范围:{}'.format(stop_loss_range))
之前小节构造因子对象的序列使用字典形式装载参数和class,如下所示
{'stop_loss_n': 1.0, 'stop_win_n': 3.0,
'class': AbuFactorAtrNStop},
上面sell_atr_nstop_factor_grid使用类似的方法构造字典对象,区别就是所有字典中的元素都变成可迭代的序列了。
使用类似的方式设置AbuFactorPreAtrNStop与AbuFactorCloseAtrNStop参数设置范围,如下所示:
In [3]:
close_atr_range = np.arange(1.0, 4.0, 0.5)
pre_atr_range = np.arange(1.0, 3.5, 0.5)
sell_atr_pre_factor_grid = {
'class': [AbuFactorPreAtrNStop],
'pre_atr_n' : pre_atr_range
}
sell_atr_close_factor_grid = {
'class': [AbuFactorCloseAtrNStop],
'close_atr_n' : close_atr_range
}
print('暴跌保护止损参数pre_atr_n设置范围:{}'.format(pre_atr_range))
print('盈利保护止盈参数close_atr_n设置范围:{}'.format(close_atr_range))
在参数参数设置范围确定了的前提下即可以开始对参数进行组合,代码如下所示:
In [4]:
sell_factors_product = ABuGridHelper.gen_factor_grid(
ABuGridHelper.K_GEN_FACTOR_PARAMS_SELL,
[sell_atr_nstop_factor_grid, sell_atr_pre_factor_grid, sell_atr_close_factor_grid], need_empty_sell=True)
print('卖出因子参数共有{}种组合方式'.format(len(sell_factors_product)))
print('卖出因子组合0: 形式为{}'.format(sell_factors_product[0]))
上述代码使用ABuGridHelper.gen_factor_grid对参数进行组合,结果共组合出477种包括不同参数的组合,不同因子的组合,也有完全不使用任何因子的组合。
相似方式使用ABuGridHelper生成不同买入参数的因子排列组合, 这里只使用42日、60日作为备选参数。读者可使用类似bk_days = np.arange(20, 130, 10)方式生成更多的买入参数,更可以自己实现其它的买入策略与AbuFactorBuyBreak一同做grid,但是在之后阶段运行Grid Search速度就会越慢。
In [5]:
buy_bk_factor_grid1 = {
'class': [AbuFactorBuyBreak],
'xd': [42]
}
buy_bk_factor_grid2 = {
'class': [AbuFactorBuyBreak],
'xd': [60]
}
buy_factors_product = ABuGridHelper.gen_factor_grid(
ABuGridHelper.K_GEN_FACTOR_PARAMS_BUY, [buy_bk_factor_grid1, buy_bk_factor_grid2])
print('买入因子参数共有{}种组合方式'.format(len(buy_factors_product)))
print('买入因子组合形式为{}'.format(buy_factors_product))
由于只选用42d、60d为参数,所以只有三种排列组合,也就是分别为:
买入因子有477种组合,卖出因子有3种组合,两者再组合将有477 x 3 = 1431中因子组合
In [6]:
print('组合因子参数数量{}'.format(len(buy_factors_product) * len(sell_factors_product) ))
下面代码使用abupy中的GridSearch类进行最优参数寻找,从GridSearch类参数可以看到除了buy_factors_product、sell_factors_product外,还有stock_pickers_product(选股因子排列组合)本例没有用到,读者可自行尝试。
In [7]:
read_cash = 1000000
choice_symbols = ['usNOAH', 'usSFUN', 'usBIDU', 'usAAPL', 'usGOOG',
'usTSLA', 'usWUBA', 'usVIPS']
grid_search = GridSearch(read_cash, choice_symbols,
buy_factors_product=buy_factors_product,
sell_factors_product=sell_factors_product)
下面开始通过fit函数开始寻找最优,第一次运行select:run gird search,然后点击run select,如果已经运行过可select:load score cache直接从缓存数据读取
备注:如果第一次运行选择run gird search下面的运行耗时大约1小时多,建议电脑空闲时运行
In [8]:
scores = None
score_tuple_array = None
def run_grid_search():
global scores, score_tuple_array
# 运行GridSearch n_jobs=-1启动cpu个数的进程数
scores, score_tuple_array = grid_search.fit(n_jobs=-1)
# 运行完成输出的score_tuple_array可以使用dump_pickle保存在本地,以方便之后使用
ABuFileUtil.dump_pickle(score_tuple_array, '../gen/score_tuple_array')
def load_score_cache():
"""有本地数据score_tuple_array后,即可以从本地缓存读取score_tuple_array"""
global scores, score_tuple_array
with AbuBlockProgress('load score cache'):
score_tuple_array = ABuFileUtil.load_pickle('../gen/score_tuple_array')
if not hasattr(grid_search, 'best_score_tuple_grid'):
# load_pickle的grid_search没有赋予best_score_tuple_grid,这里补上
from abupy import make_scorer, WrsmScorer
scores = make_scorer(score_tuple_array, WrsmScorer)
grid_search.best_score_tuple_grid = score_tuple_array[scores.index[-1]]
print('load complete!')
def select(select):
if select == 'run gird search':
run_grid_search()
else: # load score cache
load_score_cache()
_ = ipywidgets.interact_manual(select, select=['run gird search', 'load score cache'])
下面使用type查看返回的score_tuple_array类型为list,score_tuple_array中的元素类型为AbuScoreTuple:
In [9]:
type(score_tuple_array), type(score_tuple_array[0])
Out[9]:
上面说过组合数量477 x 3 = 1431中因子组合:
最终的评分结果也应该scores是有1431种,下面开始验证:
In [10]:
print('最终评分结果数量{}'.format(len(scores)))
grid_search中保存了得到分数最高的对象best_score_tuple_grid,可以利用它直接用AbuMetricsBase可视化最优参数结果,如图:
In [13]:
from abupy import AbuMetricsBase
best_score_tuple_grid = grid_search.best_score_tuple_grid
AbuMetricsBase.show_general(best_score_tuple_grid.orders_pd, best_score_tuple_grid.action_pd,
best_score_tuple_grid.capital, best_score_tuple_grid.benchmark)
读者可能会有的疑问1:哪里来的分数,怎么评定的分数。
在GridSearch类 fit()函数中可以看到第二个参数score_class需要一个判分类,在fit()中最后几行使用ABuMetricsScore.make_scorer()函数将所有返回的结果score_tuple_array使用判分类WrsmScorer对结果打分数。
打分数实现的基本思路为:如果想根据sharpe值的结果来对最优参数下判断,但是sharpe值多大可以判定为100分?多大可以判断为50分呢?我们无法确定,所以将所有sharpe结果排序,由于sharpe值越大越好,所以排序结果对应分数由0至1,这样就可以得到某一个具体参数组合的sharpe值的分数。这样的话使用多个评分标准, 比如这里使用'win_rate'、 'returns'、 'sharpe'、'max_drawdown'四种,就可以分别计算出某个参数组合对应的度量指标评分,再乘以分配给它们的权重就可以得到最终结果分数。
具体请阅读GridSearch.fit,AbuBaseScorer,WrsmScorer源代码实现
如下代码我们实例化一个评分类WrsmScorer,它的参数为之前GridSearch返回的score_tuple_array对象:
In [12]:
from abupy import WrsmScorer
scorer = WrsmScorer(score_tuple_array)
如下可以看到scorer中的score_pd是由评分的度量指标数值,以及这个具体数据对应项所得的分数组成:
In [14]:
scorer.fit_score()
scorer.score_pd.tail()
Out[14]:
由于AbuBaseScorer 中fit_score()函数的实现只是对score_pd的'score'项进行排序后返回score,这样最终的结果为分数及对应score_tuple_array的序列号,从上面输出可以看出665为最优参数序号。
In [17]:
# 实例化WrsmScorer,参数weights,只有第二项为1,其他都是0,
# 代表只考虑投资回报来评分
scorer = WrsmScorer(score_tuple_array, weights=[0, 1, 0, 0])
# 返回排序后的队列
scorer_returns_max = scorer.fit_score()
# 因为是倒序排序,所以index最后一个为最优参数
best_score_tuple_grid = score_tuple_array[scorer_returns_max.index[-1]]
# 由于篇幅,最优结果只打印文字信息
AbuMetricsBase.show_general(best_score_tuple_grid.orders_pd,
best_score_tuple_grid.action_pd,
best_score_tuple_grid.capital,
best_score_tuple_grid.benchmark,
only_info=True)
可以看到只考虑投资回报来评分的话,上面策略收益: 50.44%为最高。
下面通过best_score_tuple_grid.buy_factors,best_score_tuple_grid.sell_factors看一下这个收益的买入因子参数、卖出因子参数,如下所示,可以发现它的因子参数组合与我在上一节开始使用的参数是一摸一样的。
In [18]:
best_score_tuple_grid.buy_factors, best_score_tuple_grid.sell_factors
Out[18]:
下面看一下只考虑胜率来评分的结果,从结果可以看到虽然胜率达到了85%以上,但是收益并不高:
In [19]:
# 只有第一项为1,其他都是0代表只考虑胜率来评分
scorer = WrsmScorer(score_tuple_array, weights=[1, 0, 0, 0])
# 返回按照评分排序后的队列
scorer_returns_max = scorer.fit_score()
# index[-1]为最优参数序号
best_score_tuple_grid = score_tuple_array[scorer_returns_max.index[-1]]
AbuMetricsBase.show_general(best_score_tuple_grid.orders_pd,
best_score_tuple_grid.action_pd,
best_score_tuple_grid.capital,
best_score_tuple_grid.benchmark,
only_info=False)
# 最后打印出只考虑胜率下最优结果使用的买入策略和卖出策略
best_score_tuple_grid.buy_factors, best_score_tuple_grid.sell_factors
Out[19]:
从输出可以看到买入策略只使用60天突破买入作为买入信号,卖出只以保护利润的止盈ABuFactorCloseAtrNStop发出卖出信号,其实这种策略就是没有止损的策略。很像大多数普通交易者的交易模式,亏损了的交易不卖出,持有直到可以再次盈利为。这样的方式投资者的胜率非常高,我之前看过几个朋友的交易账户,发现他们交易的胜率非常高,但他们的账户最终都是亏损的,我认为交易中最虚幻的就是胜率,但是大多数人追求的反而是胜率。如果说股票市场最终的投资结果90%的人将亏损收场话,那我相信这90%的人胜率大多数都为超过50%甚至更高,那么如果我们想最终战胜市场的话,我们的有效投资策略是不是应该是胜率低于50%的呢?
上面的评分全部都是使用abupy中内置的WrsmScorer做为评分类,其从四个维度:胜率、sharpe、投资回报、最大回撤综合评定策略的分数,通过调整权重来达到各种评定效果,但是如果用户想要从其它的维度来综合评分的话就需要自定义评分类。
上一节中最后示例扩展自定义度量类中编写了MetricsDemo度量类,它扩展了AbuMetricsBase度量类,添加了交易手续费相关度量值,以及可视化方法 ,下面的示例编写评分类将使用手续费做为一个度量维度对grid结果进行评分,代码如下所示:
In [15]:
from abupy import AbuBaseScorer
class DemoScorer(AbuBaseScorer):
def _init_self_begin(self, *arg, **kwargs):
"""胜率,策略收益,手续费组成select_score_func"""
self.select_score_func = lambda metrics: [metrics.win_rate, metrics.algorithm_period_returns,
metrics.commission_sum]
self.columns_name = ['win_rate', 'returns', 'commission']
self.weights_cnt = len(self.columns_name)
def _init_self_end(self, *arg, **kwargs):
"""
_init_self_end这里一般的任务是将score_pd中需要反转的反转,默认是数据越大越好,有些是越小越好,
类似make_scorer(xxx, greater_is_better=True)中的参数greater_is_better的作用:
sign = 1 if greater_is_better else -1
"""
self.score_pd['commission'] = -self.score_pd['commission']
上面的代码DemoScorer即实现了自定义评分类:
本例_init_self_end中使用:
self.score_pd['commission'] = -self.score_pd['commission']
将手续费数值进行反转,因为本例度量的标准是手续费的花销越小代表结果越好,所以需要在_init_self_end中将手续费这一列变成负数。
下面构建DemoScorer,注意传递了关键字参数metrics_class,使用MetricsDemo,默认不传递metrics_class将使用AbuMetricsBase做为度量类提供度量值,但是DemoScorer中需要使用metrics.commission_sum,所以这里必须使用MetricsDemo,代码如下所示:
备注:已将MetricsDemo做为abupy内置策略示例因子在项目中,所以本节不重复编写,直接从abupy中import,如下所示
In [17]:
from abupy import MetricsDemo
scorer = DemoScorer(score_tuple_array, metrics_class=MetricsDemo)
# 返回按照评分排序后的队列
scorer_returns_max = scorer.fit_score()
scorer.score_pd.sort_values(by='score_commission').tail()
Out[17]:
上面代码使用DemoScorer对grid的结果进行评分,从score_pd.tail()中最后一行可以看到commission=-1179.97, 即手续费总开销为1179.97的score_commission评分结果在手续费评分结果中是满分1
In [19]:
scorer.score_pd.sort_values(by='score_commission').head()
Out[19]:
从score_pd.head()中可以看到手续费开销越大的在手续费分数score_commission这一栏中的得分结果就越低, 可想而知如果使用:
scorer = DemoScorer(score_tuple_array, metrics_class=MetricsDemo, weights=[0, 0, 1])
将评分权重都放在手续费的开销上,那么评分的结果最后的即是不怎么进行买入交易,或者买入后不进行卖出交易的参数组合了,读者可自行尝试。
关于评分类的更多细节请自行阅读源AbuBaseScorer
小结:对于交易系统的优化,最优参数的选择问题,首先我们要明确所有的参数拟合都是基于历史数据的,即拟合一组最优参数使其对特定历史或者特定一些股票的回测结果趋于完美的实际意义并不大,有时反而适得其反,但是大粒度的统计意义依然具备。比如上面使用的一些因子的参数你设置为100或者其他不靠谱的数那肯定是不合适的,在统计范围内来限制参数的有限个解是有意义的,但是真实的最优参数却是不存在的,不要在最优参数上陷入误区,适可而止,掌握好度,是做好每一件事情的关键。
abu量化系统文档教程持续更新中,请关注公众号中的更新提醒。
更多关于量化交易相关请阅读《量化交易之路》
更多关于量化交易与机器学习相关请阅读《机器学习之路》
更多关于abu量化系统请关注微信公众号: abu_quant
如有任何问题也可在公众号中联系我的个人微信号。