失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Python量化交易学习笔记(36)——backtrader多股回测避坑3

Python量化交易学习笔记(36)——backtrader多股回测避坑3

时间:2019-02-04 23:25:31

相关推荐

Python量化交易学习笔记(36)——backtrader多股回测避坑3

本文继续记录多股回测时可能遇到的异常情况。

坑描述

多股回测时,当日期达到所有股票的技术指标都能够计算出有效值后,backtrader才开始进行回测。由于这种逻辑的存在,如果某些股票在回测周期的最后几天才能计算出技术指标,那么就会导致回测只在最后几天进行,前面大片回测时间被浪费。

坑重现

为了重现上述现象,做如下回测设定(与笔记(35)相同):

使用20日均线作为买卖条件的判断标准:

MIN_PERIOD = 20# 可配置策略参数params = dict(period = MIN_PERIOD, # 均线周期stake = 100, # 单笔交易股票数目)def __init__(self):self.inds = dict()for i, d in enumerate(self.datas):self.inds[d] = bt.ind.SMA(d.close, period=self.p.period)

买入条件:收盘价高于20日均线

if not len(pos): # 不在场内,则可以买入if d.close[0] > self.inds[d][0]: # 达到买入条件self.buy(data = d, size = self.p.stake) # 买买买

卖出条件:收盘价低于20日均线

elif d.close[0] < self.inds[d][0]:# 达到卖出条件self.close(data = d)# 卖卖卖

回测周期:1月1日至12月31日

fromdate = datetime.datetime(, 1, 1)todate = datetime.datetime(, 12, 31)

股票组合:使用[‘002321’, ‘002322’]的组合与[‘002321’, ‘002322’, ‘002323’]的组合做对比

stk_pools = ['002321', '002322']#stk_pools = ['002321', '002322', '002323']

在回测周期内,002321日K线共244根,002322日K线共244根,002323日K线共20根(长期停盘)

当使用组合[‘002321’, ‘002322’]进行回测时,策略的next方法从1月29日开始运行。

当使用组合[‘002321’, ‘002322’, ‘002323’]进行回测时,策略的next方法从12月31日开始运行。

坑分析

002321和002322在回测周期内(1月1日至12月31日)共有244根日K线,第1根K线出现在1月2日,第20根K线出现在1月29日,20日均线指标从1月29日起可计算得到有效值

002323在回测周期内共有20根K线,第1根K线出现在12月4日,第20根K线出现在12月31日,20日均线指标从12月31日起可计算得到有效值

当回测组合[‘002321’, ‘002322’]时,所有回测股票自1月29日起,都能计算得到回测策略所使用技术指标的有效值,因此回测从1月29日开始进行。

当回测组合[‘002321’, ‘002322’, ‘002323’]时,所有回测股票自12月31日起,都能计算得到回测策略所使用技术指标的有效值,因此回测从12月31日开始进行。而回测周期到12月31日结束,也就是由于002323参与回测,整个回测过程只运行了一天,有效回测周期大幅缩减,可以认为回测失败。

经过backtrader源码分析,多股回测时,当日期达到所有股票的技术指标都能够计算出有效值后,backtrader才开始进行回测。

在backtrader中定义了最小周期的概念,当指标经过最小周期时,才能计算出有效值。例如,20日均线的最小周期就是20,即在经过20根K线时,才能计算出20日均线的第一个有效值。那么,在多股回测时,就要求所有参与回测的股票,必须都经过各自所有指标的最小周期,回测才会开始。

当经过最小周期计算出指标的有效值后,股票后续由于停盘等原因造成K线缺失,不会影响回测过程的正常执行,策略的next方法仍会按日依次执行。

避坑方案

为了避免由于个别股票的参与,导致回测过程受到影响,出现有效回测周期大幅缩减的异常情况,可以参考以下方案(在实验中,将当前上市交易的所有股票都考虑在回测范围内):

方案1:

统计回测周期内所有股票的K线数目,按照K线的数目对股票进行分组。这样绝大多数股票由于未停牌应该具有相同的K线数目(不考虑九十年代大量股票未上市的情况)而被分到一组中,并且该分组内所包含的股票数量最多。因此可以对该分组内的股票进行回测。

根据回测周期内的K线数目对股票进行分组

# 根据回测周期内的K线数目对股票进行分组def analyze_backtest_bar_size(fromdate, todate):# 读入股票代码stk_code_file = '../TQDat/TQDownv1/data/stock_code_update.csv'stk_pools = pd.read_csv(stk_code_file, encoding = 'gbk')total_size = stk_pools.shape[0] # 当前上市股票总数# 字典,key为K线的根数,value是一个list,里面包含拥有key根K线的股票代码size_dict = defaultdict(list)# 遍历所有股票,分析K线数目for i in range(total_size):stk_code = stk_pools['code'][stk_pools.index[i]]stk_code = '%06d' % stk_code# 读入数据datapath = '../TQDat/day/stk/' + stk_code + '.csv'size_dict[bar_size(datapath, fromdate.strftime('%Y-%m-%d'), todate.strftime('%Y-%m-%d'))].append(stk_code)return size_dict

分组结果保存在字典size_dict中。在该字典中,key是各个分组中每个股票在回测周期内的K线数目,value是对应的股票列表,即形式为:

{..., 243 : ['000010', '000422', '000504', ...], 244 : ['000001', '000002', '000005', ...], ...}

所表示的意思是,000010、000422、000504等股票在回测周期(1月1日至12月31日)内有243根日K线,000001、000002、000005等股票在回测周期内有244根日K线。

下一步将选择分组内股票数目最多的组参与回测。

获取回测股票代码

# 获取回测股票代码def backtest_stks(min_period, fromdate, todate):size_dict = analyze_backtest_bar_size(fromdate, todate)# 按拥有相同K线数目股票列表大小对字典进行排序sorted_size_list = sorted(size_dict.items(), key = lambda x:len(x[1]), reverse = True)# 取出拥有相同K线数目的股票列表中,股票数目最多的列表,其key值就是列表中股票参与回测的K线根数backtest_bar_size = sorted_size_list[0][0]# 获取回测股票列表return size_dict[backtest_bar_size]

在得到字典size_dict之后,对字典按照value值所表示的列表的大小进行排序,即按照每个分组中股票数目的多少进行排序,然后选取股票数目最多的一组参与回测。

在所有股票中,回测周期1月1日至12月31日内,K线数目最多为244根。全年均无停盘的情况下,股票的K线数目为244。

而拥有244根K线的这组股票数目确实也是最多的,达到了3098只股票(当前上市交易的股票总数目为3860)。

最后,用这3098只股票进行回测,程序单机运行约40分钟,回测的最终市值为1080960.00元,年化收益率8.10%(凑合吧)。

方案2:

方案1中,对分组得到的股票数目最多的这组股票进行了回测,已经可以选出绝大部分的股票。考虑到上文提及“当经过最小周期计算出指标的有效值后,股票后续由于停盘等原因造成K线缺失,不会影响回测过程的正常执行,策略的next方法仍会按日依次执行”,那么有更多的股票是可以参与到回测过程中的。

例如,在本实验中,000001在回测周期内有244根K线,那么它将参与回测过程中,1月29日为最小周期的最后一天,策略的next方法从1月29日起开始执行。000010在回测周期内有243根K线,仅在4月25日停盘一天,1月29日也是它的最小周期的最后一天,那么000010参与回测后,策略的next方法仍可以从1月29日起开始执行,且不会影响后续回测过程的正常执行。因此,000010也可以参与到回测中。

按照以下步骤筛选参与回测的股票:

根据回测周期内的K线数目对股票进行分组,获取股票数目最多的分组(参考方案1)

在股票数目最多的分组中,选取一只股票作为参考股票

# 获取其中一只股票作为参考ref_stk = size_dict[backtest_bar_size][0]

size_dict为分组信息,backtest_bar_size为回测最大的日线根数(实验中为244),索引[0]表示取分组中的第一只股票。

计算参考股票最小周期的起止日期

# 计算最小周期的起止日期def cal_minperiod_fromtodate(stk, min_period, fromdate, todate):# 读取股票数据datapath = '../TQDat/day/stk/' + stk + '.csv'stk_df = pd.read_csv(datapath, encoding = 'gbk')# 获取fromdate后的数据stk_df = stk_df[(stk_df['date'] >= fromdate.strftime('%Y-%m-%d'))]# 获取fromdate后的第一个日期stk_from = stk_df.iloc[0].at['date']# 获取最小周期内的最后一个日期stk_to = stk_df.iloc[min_period - 1].at['date']# 判断是否超过结束日期check_to = datetime.datetime.strptime(stk_from, '%Y-%m-%d')if check_to > todate:print('Date Error!')sys.exit(1)return stk_from, stk_to

在读取参考股票的数据后,在回测周期内的第一根K线的日期为最小周期的开始日期(也是回测的真实开始日期),在此之后的最小周期减1得到日期为最小周期的结束日期。

最小周期的起止日期用于后续筛选参与回测的股票,只有最小周期的起止日期与参考股票完全一致的股票才应参与回测。

获取参与回测的股票列表

# 判断单只股票是否参与回测def check_stk(stk, min_period, fromdate, todate):datapath = '../TQDat/day/stk/' + stk + '.csv'return min_period == bar_size(datapath, fromdate, todate)# 获取回测股票列表def cal_stk_list(size_dict, min_period, backtest_bar_size, fromdate, todate):# 返回值stk_list = []# 遍历字典中的股票for k, v in size_dict.items():# K线数目不足最小周期的直接剔除if k < min_period:continue# K线数目大于等于最多共有K线数目的股票参加回测elif k >= backtest_bar_size:stk_list.extend(v)# 其他情况下,判断股票是否包含参考股票最初最小周期数目的K线,若包含则参与回测else:for stk in v:if check_stk(stk, min_period, fromdate, todate):stk_list.append(stk)return stk_list

对分组信息size_dict分三类进行处理:

对于K线数目少于最小周期的股票,进行直接剔除

对于K线数目大于等于最多共有K线数目(实验中为244)的股票,将参与回测

其他情况下,判断股票是否与参考股票拥有相同的最小周期起止日期,若是则参与回测,否则剔除

按照上面的步骤,方案2共筛选出3492只股票参与回测(较方案1增加了394只),程序单机运行约60分钟,回测的最终市值为1077636.00元,年化收益率7.76%(也还行吧)。

总结

在多股回测时,要求所有参与回测的股票,必须都经过各自所有技术指标的最小周期,能够计算出所有技术指标的有效值后,回测才会开始。

如果不针对指标的最小周期问题,对股票进行筛选剔除,可能会造成有效回测周期大幅缩减。

当经过最小周期计算出指标的有效值后,股票后续由于停盘等原因造成K线缺失,不会影响回测过程的正常执行,策略的next方法仍会按日依次执行。

可按回测周期内K线数目对股票分组,对股票数目最多的这组进行回测,并筛选出其他组中与该组拥有相同最小周期起止日期的股票参与回测,以提升参与回测的股票总数。

方案2代码:

from __future__ import (absolute_import, division, print_function,unicode_literals)import sysimport datetime # 用于datetime对象操作import os.path # 用于管理路径import backtrader as bt # 引入backtrader框架import pandas as pdfrom collections import defaultdictMIN_PERIOD = 20# 统计回测周期内K线数量def bar_size(datapath, fromdate, todate):df = pd.read_csv(datapath)return len(df[(df['date'] >= fromdate) & (df['date'] <= todate)])# 计算最小周期的起止日期def cal_minperiod_fromtodate(stk, min_period, fromdate, todate):# 读取股票数据datapath = '../TQDat/day/stk/' + stk + '.csv'stk_df = pd.read_csv(datapath, encoding = 'gbk')# 获取fromdate后的数据stk_df = stk_df[(stk_df['date'] >= fromdate.strftime('%Y-%m-%d'))]# 获取fromdate后的第一个日期stk_from = stk_df.iloc[0].at['date']# 获取最小周期内的最后一个日期stk_to = stk_df.iloc[min_period - 1].at['date']# 判断是否超过结束日期check_to = datetime.datetime.strptime(stk_from, '%Y-%m-%d')if check_to > todate:print('Date Error!')sys.exit(1)return stk_from, stk_to# 判断单只股票是否参与回测def check_stk(stk, min_period, fromdate, todate):datapath = '../TQDat/day/stk/' + stk + '.csv'return min_period == bar_size(datapath, fromdate, todate)# 获取回测股票列表def cal_stk_list(size_dict, min_period, backtest_bar_size, fromdate, todate):# 返回值stk_list = []# 遍历字典中的股票for k, v in size_dict.items():# K线数目不足最小周期的直接剔除if k < min_period:continue# K线数目大于等于最多共有K线数目的股票参加回测elif k >= backtest_bar_size:stk_list.extend(v)# 其他情况下,判断股票是否包含参考股票最初最小周期数目的K线,若包含则参与回测else:for stk in v:if check_stk(stk, min_period, fromdate, todate):stk_list.append(stk)return stk_list# 根据回测周期内的K线数目对股票进行分组def analyze_backtest_bar_size(fromdate, todate):# 读入股票代码stk_code_file = '../TQDat/TQDownv1/data/stock_code_update.csv'stk_pools = pd.read_csv(stk_code_file, encoding = 'gbk')total_size = stk_pools.shape[0] # 当前上市股票总数# 字典,key为K线的根数,value是一个list,里面包含拥有key根K线的股票代码size_dict = defaultdict(list)# 遍历所有股票,分析K线数目for i in range(total_size):stk_code = stk_pools['code'][stk_pools.index[i]]stk_code = '%06d' % stk_code# 读入数据datapath = '../TQDat/day/stk/' + stk_code + '.csv'size_dict[bar_size(datapath, fromdate.strftime('%Y-%m-%d'), todate.strftime('%Y-%m-%d'))].append(stk_code)return size_dict# 获取回测股票代码def backtest_stks(min_period, fromdate, todate):size_dict = analyze_backtest_bar_size(fromdate, todate)# 按拥有相同K线数目股票列表大小对字典进行排序sorted_size_list = sorted(size_dict.items(), key = lambda x:len(x[1]), reverse = True)# 取出拥有相同K线数目的股票列表中,股票数目最多的列表,其key值就是列表中股票参与回测的K线根数backtest_bar_size = sorted_size_list[0][0]# 获取其中一只股票作为参考ref_stk = size_dict[backtest_bar_size][0]# 获取最小周期的起止时间stk_from, stk_to = cal_minperiod_fromtodate(ref_stk, min_period, fromdate, todate)# 获取回测股票列表return cal_stk_list(size_dict, min_period, backtest_bar_size, stk_from, stk_to)# 创建策略class SmaStrategy(bt.Strategy):# 可配置策略参数params = dict(period = MIN_PERIOD, # 均线周期stake = 100, # 单笔交易股票数目)def __init__(self):self.inds = dict()for i, d in enumerate(self.datas):self.inds[d] = bt.ind.SMA(d.close, period=self.p.period)def next(self):print(self.datetime.date())for i, d in enumerate(self.datas):pos = self.getposition(d)if not len(pos): # 不在场内,则可以买入if d.close[0] > self.inds[d][0]: # 达到买入条件self.buy(data = d, size = self.p.stake) # 买买买elif d.close[0] < self.inds[d][0]:# 达到卖出条件self.close(data = d)# 卖卖卖fromdate = datetime.datetime(, 1, 1)todate = datetime.datetime(, 12, 31)cerebro = bt.Cerebro() # 创建cerebrofor stk_code in backtest_stks(MIN_PERIOD, fromdate, todate):# 读入数据datapath = '../TQDat/day/stk/' + stk_code + '.csv'#print(stk_code)# 创建价格数据data = bt.feeds.GenericCSVData(dataname = datapath,fromdate = fromdate,# option 2#todate = todate,todate = todate + datetime.timedelta(days=1),nullvalue = 0.0,dtformat = ('%Y-%m-%d'),datetime = 0,open = 1,high = 2,low = 3,close = 4,volume = 5,openinterest = -1)# 在Cerebro中添加股票数据cerebro.adddata(data, name = stk_code)cerebro.broker.setcash(1000000.0)# 设置启动资金cerebro.addstrategy(SmaStrategy) # 添加策略cerebro.run()# 遍历所有数据print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

为了便于相互交流学习,已建微信群,感兴趣的读者请加微信。

如果觉得《Python量化交易学习笔记(36)——backtrader多股回测避坑3》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。