在笔记(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线数量
首先读入股票数据,然后将日期数据转化为list,使用二分查找法找到开始、结束日期的位置,最后计算得到有效K线数目,代码如下:
# 统计回测周期内K线数量
def bar_size(datapath, fromdate, todate):
df = pd.read_csv(datapath)
# 将所有日期转化为datetime的list
date_list = list(map(lambda x:datetime.datetime.strptime(x, '%Y-%m-%d'),
df['date'].to_list()))
# 使用二分查找确定起止日期索引值
start_index = binary_search(date_list, fromdate)
end_index = binary_search(date_list, todate)
return end_index - start_index + 1
二分查找代码如下:
# 二分查找,确定所查找日期索引值
def binary_search(lst, target):
start = 0
end = len(lst) - 1
while start <= end:
mid = (start + end) // 2
if target > lst[mid]:
start = mid + 1
elif target < lst[mid]:
end = mid - 1
else:
return mid
return min(abs(start), abs(end))
- 剔除无效股票
通过调用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
# 二分查找,确定所查找日期索引值
def binary_search(lst, target):
start = 0
end = len(lst) - 1
while start <= end:
mid = (start + end) // 2
if target > lst[mid]:
start = mid + 1
elif target < lst[mid]:
end = mid - 1
else:
return mid
return min(abs(start), abs(end))
# 统计回测周期内K线数量
def bar_size(datapath, fromdate, todate):
df = pd.read_csv(datapath)
# 将所有日期转化为datetime的list
date_list = list(map(lambda x:datetime.datetime.strptime(x, '%Y-%m-%d'),
df['date'].to_list()))
# 使用二分查找确定起止日期索引值
start_index = binary_search(date_list, fromdate)
end_index = binary_search(date_list, todate)
return end_index - start_index + 1
# 创建策略
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())
转载:https://blog.csdn.net/m0_46603114/article/details/106946330