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

在笔记(17)中,对backtrader多只股票同时进行策略回测的过程进行了记录,当时只进行了少量股票的同时回测,如果增加参与回测的股票数目,就会出现各种异常的情况。

从本文开始,将用几篇笔记记录在多股回测时发现的几个坑,并给出避坑方案。

坑描述

回测周期内,某些股票的可用K线数量少于计算技术指标时的最小周期数。当出现这种情况时,backtrader不会对这些股票进行清洗剔除,依然会计算相应的技术指标,从而造成了访问数组越界的情况,典型报错信息为:

IndexError: array assignment index out of range

坑重现

为了重现上述现象,做如下回测设定:

  • 使用30日均线作为买卖条件的判断标准:
MIN_PERIOD = 30

		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)
  • 买入条件:收盘价高于30日均线
            if not len(pos):                                 # 不在场内,则可以买入
                if d.close[0] > self.inds[d][0]:             # 达到买入条件
                    self.buy(data = d, size = self.p.stake)  # 买买买
  • 卖出条件:收盘价低于30日均线
            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根(长期停盘)。显然,使用002323的20根K线,是无法计算30日均线的

当使用组合[‘002321’, ‘002322’]进行回测时,程序可以正常运行;当使用组合[‘002321’, ‘002322’, ‘002323’]进行回测时,程序会报本文开始部分提到的IndexError

避坑方案

为了确保参与回测的每只股票都能计算得到有效的技术指标,可以先计算在回测周期内有效K线的数目,如果K线数目足以计算策略使用的技术指标,则该股票参与回测;否则,剔除该股票。

  • 统计回测周期内K线数量

首先读入股票数据,然后获取在起止日期内的数据长度,代码如下:

# 统计回测周期内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'))])
  • 剔除无效股票

通过调用bar_size方法,计算回测周期内有效K线数目,如果有效K线数目能够支持计算回测过程所需的技术指标,则将该股票数据添加到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)
    todate = datetime.datetime(2019, 12, 31)

    # 剔除无效股票
    if MIN_PERIOD > bar_size(datapath, fromdate, todate):
        continue
    # 创建价格数据
    data = bt.feeds.GenericCSVData(
            dataname = datapath,
            fromdate = fromdate,
            todate = todate,
            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)
  • 回测结果

通过以上的避坑操作,使用[‘002321’, ‘002322’]的组合与[‘002321’, ‘002322’, ‘002323’]的组合进行回测时,可以得到相同的回测结果。

  • 结果分析

在对组合[‘002321’, ‘002322’, ‘002323’]进行回测时,由于002323在回测周期内只有20根日K线,无法计算策略中30日均线值,因此被剔除未参与回测

总结

  • 在多股回测中,会对参与回测的所有股票计算策略所使用的技术指标值。

  • 在回测周期中,某些股票由于未上市、停盘的因素,有效K线数量小于计算技术指标所需K线数量,会导致程序报错(IndexError: array assignment index out of range)。

  • 为了保证程序正常运行,可在回测前对数据进行清洗,剔除回测周期内有效K线数量不足的股票。

  • 不同计算指标对K线数目的需要略有不同(后续文章再记录)。

backtrader多股回测避坑1代码:

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 = 30

# 统计回测周期内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):
        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)
    todate = datetime.datetime(2019, 12, 31)

    # 剔除无效股票
    if MIN_PERIOD > bar_size(datapath, fromdate, todate):
        continue
    # 创建价格数据
    data = bt.feeds.GenericCSVData(
            dataname = datapath,
            fromdate = fromdate,
            todate = todate,
            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量化交易学习笔记(34)——backtrader多股回测避坑1_第1张图片

你可能感兴趣的:(Python量化交易)