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

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

坑描述

  1. backtrader在读取日线数据时,会自动给date数据添加“时:分:秒.毫秒(23:59:59.999990)”信息。

  2. 而通常用户在指定回测周期的开始和结束日期时,只会精确到日,时分秒信息会被backtrader默认以0补全。

由于上述两个事实的存在,假如用户指定回测周期的结束日期有日线数据(由于非交易日、停盘等原因,可能没有日线数据),那么在backtrader中,回测周期的结束时间就会被设定为该日的00:00:00,而backtrader读取的该日日线数据的时间标识为该日的23:59:59.999990大于了设定的结束时间,因此该日的日线数据不会参与回测。这样就导致了参与回测的日K线数据,比预期少一根,从而引发计算技术指标时出现读写数组越界错误,典型的报错信息为:

IndexError: array assignment index out of range

坑重现

为了重现上述现象,做如下回测设定(与笔记(34)基本一致,只是将30日线变为20日线来判断交易条件):

  • 使用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’]进行回测时,程序可以正常运行;当使用组合[‘002321’, ‘002322’, ‘002323’]进行回测时,程序会报错IndexError而退出。

坑分析

002321和002322在回测周期内(2019年1月1日至2019年12月31日)都有244根日K线,因此足以有效计算出20日均线指标进行回测。而002323在回测周期内有20根K线,本应能够计算出20日均线数据,然而程序却报错退出。

正如本文开头提及的情况:

  1. backtrader在读入日线数据时,会自动给date数据添加“时:分:秒.毫秒(23:59:59.999990)”信息。那么,2019年12月31日的K线数据在backtrader中标记的时间为2019年12月31日23时59分59.999990秒。

  2. 设定的回测周期的截止日期为2019年12月31日,时分秒信息会被backtrader默认以0补全。那么,回测截止时间在backtrader中为2019年12月31日0时0分0秒

002323在2019年12月31日是有K线数据的,但在backtrader中,该日K线数据的时间标签(2019年12月31日23时59分59.999990秒)大于回测截止时间(2019年12月31日0时0分0秒),因此不会参与回测。这样,002323参与回测的K线数量为19,无法计算出20日均线数值,出现了笔记(34)提到的可用K线数量少于计算技术指标时的最小周期数的情况,计算指标时访问数组越界,程序报错退出。

避坑方案

按照预期,回测周期中结束日期当日的K线数据也应该参与回测。为了实现这一预期,可对结束日期进行小的修改,以下是两种可选方案:

  • 方案1:

向回测周期的结束日期后添加时分秒信息:

    # option 1
    #todate = datetime.datetime(2019, 12, 31)
    todate = datetime.datetime(2019, 12, 31, 23, 59, 59, 999990)

由于backtrader在读取日K数据时,会在日期后自动添加23时59分59.999990秒,所以这里也将结束日期后添加相同的时分秒数据。这样在做比较时,2019年12月31日的K线数据就不会因为超过了结束时间而被弃用。

  • 方案2

方案1的代码看上去不够整洁,方案2在将把数据添加到cerebro之前对结束日期进行加1日操作:

    # 创建价格数据
    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
            )

todate初始值为2019年12月31日,加1天后值为2020年1月1日,在backtrader中就会将结束时间补全为2020年1月1日0时0分0秒。此后,当backtrader读取2019年12月31日的K线数据(时间标记为2019年12月31日23时59分59.999990秒)时,该K线就会被正常读取并参与回测,同时也能保证2020年1月1日的K线不参与回测。

  • 回测结果

通过以上的避坑操作,使用[‘002321’, ‘002322’]的组合与[‘002321’, ‘002322’, ‘002323’]的组合进行回测时,程序均可以正常运行,但是运行结果不同。回测组合[‘002321’, ‘002322’]时,策略的next方法从2019年1月29日开始运行;回测组合[‘002321’, ‘002322’, ‘002323’]时,策略的next方法从2019年12月31日才开始运行。

  • 结果分析

在回测组合[‘002321’, ‘002322’, ‘002323’]时,由于程序将回测周期的结束时间进行了微小的调整,所以002323在2019年12月31日的K线数据也参与到回测过程中,这样参与回测002323的K线数目为20,满足了计算技术指标的最小需求,因此不会被清洗剔除。

但是这样的操作又引发了新坑,在将002323加入回测流程后,回测从2019年12月31日才开始,具体原因后续笔记再做分析。

总结

  • backtrader在读取日线数据时,会自动给date数据添加“时:分:秒.毫秒(23:59:59.999990)”信息。

  • 用户指定回测周期的开始和结束日期时若无时分秒信息,时分秒会被backtrader默认以0补全。

  • 在不采取措施的情况下,截止日期当日的K线数据不会参与到回测过程中,由此可能造成计算技术指标时访问数组越界,程序报错退出。

  • 可对回测周期的结束日期做小的调整,来避免以上非预期情况的发生。

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)
import datetime  # 用于datetime对象操作
import os.path  # 用于管理路径
import backtrader as bt # 引入backtrader框架
import pandas as pd

MIN_PERIOD = 20

# 统计回测周期内K线数量
def bar_size(datapath, fromdate, todate):
    df = pd.read_csv(datapath)
    return len(df[(df['date'] >= fromdate.strftime('%Y-%m-%d')) 
            & (df['date'] <= todate.strftime('%Y-%m-%d'))])

# 创建策略
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)                         # 卖卖卖

cerebro = bt.Cerebro()  # 创建cerebro

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

for stk_code in stk_pools:
    # 读入数据
    datapath = '../TQDat/day/stk/' + stk_code + '.csv'
    fromdate = datetime.datetime(2019, 1, 1)
    # option 1
    #todate = datetime.datetime(2019, 12, 31, 23, 59, 59, 999990)
    todate = datetime.datetime(2019, 12, 31)

    # 剔除无效股票
    if MIN_PERIOD > bar_size(datapath, fromdate, todate):
        continue
    # 创建价格数据
    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())

为了便于相互交流学习,已建微信群,感兴趣的读者请加微信。
在这里插入图片描述

Guess you like

Origin blog.csdn.net/m0_46603114/article/details/106962158