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

坑描述

多股回测时,当日期达到所有股票的技术指标都能够计算出有效值后,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)                         # 卖卖卖
  • 回测周期:2019年1月1日至2019年12月31日
    fromdate = datetime.datetime(2019, 1, 1)todate = datetime.datetime(2019, 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方法从2019年1月29日开始运行。

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

坑分析

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

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

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

当回测组合[‘002321’, ‘002322’, ‘002323’]时,所有回测股票自2019年12月31日起,都能计算得到回测策略所使用技术指标的有效值,因此回测从2019年12月31日开始进行。而回测周期到2019年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/TQDown2020v1/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等股票在回测周期(2019年1月1日至2019年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值所表示的列表的大小进行排序,即按照每个分组中股票数目的多少进行排序,然后选取股票数目最多的一组参与回测。

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

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

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

  • 方案2:

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

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

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

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

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

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

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

  1. 计算参考股票最小周期的起止日期
# 计算最小周期的起止日期
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得到日期为最小周期的结束日期。

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

  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 sys
import datetime  # 用于datetime对象操作
import os.path  # 用于管理路径
import backtrader as bt # 引入backtrader框架
import pandas as pd
from 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/TQDown2020v1/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(2019, 1, 1)
todate = datetime.datetime(2019, 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相关推荐

  1. Python量化交易学习笔记(1)

    Python量化交易学习笔记(1) http://zwpython.com/ http://www.topquant.vip/?p=2275 [更多参见] <zwPython,目前最好的py开发 ...

  2. Python 量化投资原来这么简单(5) — A股回测KDJ策略

    许多技术投资方面的教材,经常会用几幅上涨的图来表明某些指标的用处,实际上那些上涨的图很可能只是假象.作者为了证明他所强调的指标的作用,选定了符合该指标策略的股票上升趋势图,但实际上这些策略并不一定适合 ...

  3. Python量化交易学习笔记(33)——backtrader仓位管理

    本文将对backtrader的仓位管理进行介绍,具体以同时回测交易3只股票为例,查看每日仓位情况. 策略 买入条件:5日线金叉60日线 卖出条件:5日线死叉60日线 示例 仓位信息输出的核心代码位于策 ...

  4. Python量化交易学习笔记(18)——放量突破布林线中轨买入策略

    本文将探索新的策略回测程序,主要是为了尝试不同的技术指标在backtrader平台上的应用,为后续复杂策略的实现做准备. 本文将实现的策略是,当股票放量突破布林线中轨时进行买入,当股票收盘价低于短期均 ...

  5. Python量化交易学习笔记(2)——A股数据下载

    由于笔者只接触过A股交易,因此本系列笔记仅针对A股交易展开.笔者的计算机为Windows 10 64位操作系统. TOP极宽量化总群(124134140)群文件中提供了大量的学习资料,一眼看去有点懵圈 ...

  6. Python量化交易学习笔记(19)——连续下跌买入止盈止损卖出策略

    好友提出要验证连续下跌买入止盈止损卖出策略,本文对该策略回测和实现做分析记录. 买入条件中,连续下跌定义为收盘价连续4日低于前1日的收盘价.卖出条件中,止盈率设置为10%,止损率设置为5%.回测初始资 ...

  7. Python量化交易学习笔记(20)——保护点卖出策略

    本文主要记录保护点卖出策略,给买入的股票设立保护点,随着股票收盘价的提升,保护点不断提高,股价一旦跌破保护点,即卖出股票. 示例的买入条件为,5日线金叉60日线,且股价进行小幅回踩(较金叉日收盘价下跌 ...

  8. Python量化交易学习笔记(25)——Data Feeds扩展

    背景:需要扩展data feeds的场景 在backtrader中,data feeds中包含了被普遍认为是业界标准的几个字段: datetime open high low close volume ...

  9. Python量化交易学习笔记(14)——均线交叉策略

    本文使用均线交叉策略,对平安银行自2018年1月1日至2020年2月28日的日线数据进行回测分析. 策略会用到短期移动均线及长期移动均线两个技术指标,在backtrader自定义策略init方法中,添 ...

最新文章

  1. Objective-C学习—UIWebView的使用
  2. 2021年河北高考生成绩排名查询,2021年河北高考成绩查询网站查分网址:http://www.hebeea.edu.cn/...
  3. spyder matlab,将pycharm配置为matlab或者spyder的用法说明
  4. 【干货分享】前端面试知识点锦集02(CSS篇)——附答案
  5. linux查看和修改当前系统时间
  6. 爬取猎聘python_爬取猎聘大数据岗位相关信息--Python
  7. Spring Boot : 资源加载器
  8. python读取cad图纸_DWG文件信息的读取
  9. windows 架设SVN服务器
  10. 最简单的vscode使用入门教程
  11. 理解RESTful架构(转)
  12. win10应用程序无法启动因为应用程序的并行配置不正确解决思路
  13. 微信小程序video控件的使用
  14. linux学习工具:工欲善其事必先利其器(2)
  15. 轻型本地服务器_一小时超轻型漂移机
  16. 对话推荐CRS论文精读KBRD:Towards Knowledge-Based Recommender Dialog System
  17. 数字档案管理系统解决方案 - 数字化档案建设方案
  18. hive常用的函数以及知识
  19. 浏览器的审查元素,这是一个神奇的玩意儿
  20. 程序员自我修养阅读笔记——运行库

热门文章

  1. JAVA swing 教程 示例
  2. html+css+js实现点球球小游戏
  3. vs2017 有时候双击打不开解决办法
  4. dota2 linux文件位置,Linux下能玩魔兽世界、打DOTA2了 4K 120Hz很爽
  5. 牧师与魔鬼 动作分离版
  6. Linux命令——top相关
  7. Zabbix 监控 CPU 使用率
  8. 数论小白都能看懂的数学期望讲解
  9. JSR303分组校验
  10. 项目在tomcat里运行一段时间总是自动崩掉的问题排查与解决