在笔记(17)中,对backtrader多只股票同时进行策略回测的过程进行了记录,当时只进行了少量股票的同时回测,如果增加参与回测的股票数目,就会出现各种异常的情况。
从本文开始,将用几篇笔记记录在多股回测时发现的几个坑,并给出避坑方案。
回测周期内,某些股票的可用K线数量少于计算技术指标时的最小周期数。当出现这种情况时,backtrader不会对这些股票进行清洗剔除,依然会计算相应的技术指标,从而造成了访问数组越界的情况,典型报错信息为:
IndexError: array assignment index out of range
为了重现上述现象,做如下回测设定:
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)
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)
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线数量
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())