失眠网,内容丰富有趣,生活中的好帮手!
失眠网 > Python量化交易学习笔记(58)——backtrader多股回测的开始时间

Python量化交易学习笔记(58)——backtrader多股回测的开始时间

时间:2023-04-23 06:03:40

相关推荐

Python量化交易学习笔记(58)——backtrader多股回测的开始时间

在使用bt进行多股回测时,经常会出现回测开始的日期比预期日期要晚很多的情况,本文将结合案例,分析这一现象的原因。本文仅对实践中用到的日线回测进行分析,如要处理分时数据,可参考本文方法分析。

本文将先通过3个案例展示多股回测的开始时间的变化情况,然后通过分析源代码说明产生这种变化情况的原因。

案例

在以下3个案例中,分别使用[600035]、[600035,300412]、[600035,300412,300919]3组股票作为股票池,回测开始时间选定为1月8日,在策略的next函数中打印以下信息:

def next(self):print('next-------------------------------------------{}'.format(bt.num2date(self.lines.datetime[0])))

在回测过程中,cerebro的所有参数均使用默认值,所有的数据均通过pandas data数据导入,所需要的指标已前期计算完成,保存在pandas data中,无需backtrader在进行计算,即数据的最小周期数为1。

案例1

股票池:600035

回测开始时间:-01-08

打印结果:

next--------------------------------------------01-08 00:00:00next--------------------------------------------01-09 00:00:00...

案例2

股票池:600035,300412

回测开始时间:-01-08

打印结果:

next--------------------------------------------02-08 00:00:00next--------------------------------------------02-09 00:00:00...

案例3

股票池:600035,300412,300919

回测开始时间:-01-08

打印结果:

next--------------------------------------------12-23 00:00:00next--------------------------------------------12-24 00:00:00...

案例分析

3个案例中,除参与回测的股票池不同外,其余设置完全相同,但从打印结果可以看出,回测的开始时间相差非常大。

案例1的真实回测开始时间为-01-08,案例2的真实回测开始时间为-02-08,案例3的真实回测开始时间为-12-23。

来看一下参与回测的股票的情况:

600035,在回测开始时间-01-08有K线数据。300412,在回测开始时间-01-08没有K线数据,-01-08至-02-07停盘,-02-08恢复交易,开始有K线数据。300919,在回测开始时间-01-08没有K线数据,-12-23上市,开始有K线数据。

通过回顾个股的情况可以发现,600035在回测开始时间-01-08有K线数据,因此案例1从-01-08开始回测;300412在-02-08才开始有K线,因此案例1从-02-08开始回测;300919在-12-23才开始有K线,因此案例1从-12-23开始回测。也就是说,多股回测时,回测真实的开始时间是参与回测的所有股票,在设置的回测开始时间后,均具有最小周期个K线数据的时间(本文中的最小周期均为1)。

源码分析

这里结合案例2,即股票池为600035和300412,进行源码分析。

最小周期状态值

回测的核心代码都在strategy的next函数中,来看一下该函数的调用堆栈:

1. run, cerebro.py: 11272. runstrategies, cerebro.py: 12933. _runonce, cerebro.py: 16954. _oncepost, strategy.py: 305

_oncepost的部分源码如下:

def _oncepost(self, dt):...minperstatus = self._getminperstatus()if minperstatus < 0:self.next()elif minperstatus == 0:self.nextstart() # only called for the 1st valueelse:self.prenext()...

可以看到,_oncepost会根据最小周期状态值minperstatus来决定是调用next、nextstart还是prenext,下面展示了这3个函数默认的实现内容。

def prenext(self):'''This method will be called before the minimum period of alldatas/indicators have been meet for the strategy to start executing'''passdef nextstart(self):'''This method will be called once, exactly when the minimum period forall datas/indicators have been meet. The default behavior is to callnext'''# Called once for 1st full calculation - defaults to regular nextself.next()def next(self):'''This method will be called for all remaining data points when theminimum period for all datas/indicators have been meet.'''pass

其中,prenext在最小周期达到前别调用,默认实现为空;nextstart在最小周期达到时被调用一次,默认是调用next函数;当达到最小周期后,next被调用,进入回测逻辑,通常用户会根据自己的策略重写next函数。

了解了这3个函数的内容后,那么什么时候进入next,开始真正的策略回测,就取决于最小周期状态值minperstatus,来看一下_getminperstatus函数的代码:

def _getminperstatus(self):# check the min period status connected to datasdlens = map(operator.sub, self._minperiods, map(len, self.datas))self._minperstatus = minperstatus = max(dlens)return minperstatus

实现非常简洁,说明如下:

self._minperiods是一个列表,列表的长度为self.datas的长度,加载数据的个数,其中每个元素对应的是每个data的最小周期数(本文的案例中均为1),案例2中加载了2个数据,每个数据最小周期都为1,那么self._minperiods=[1, 1]。map(len, self.datas)使用map求取每个data已处理过的K线的数目,案例2共加载2个数据,第1个数据已处理1根K线,即len(self.datas[0])=1,第2个数据已处理0根K线,即len(self.datas[1])=0,那么map(len, self.datas)就返回由1和0两个元素组成的迭代器。使用operator.sub,对self._minperiods和map(len, self.datas)对应元素相减,返回1个迭代器,按上面的示例就会得到[1, 1] - [1, 0] = [0, 1](dlens)。最后使用max求取迭代器dlens中的最大值,示例中为1,并返回。

在回看_oncepost的源码,如果minperstatus > 0,就会调用prenext函数,默认就什么操作也没进行。

通过上面的分析可以看出,在最小周期确定的情况下,如果有部分数据K线一直未被处理(即len(self.data[x])=0,那么max(self._minperiods - map(len, self.datas)) > 0),则会使求得的最小周期状态值minperstatus一直大于0,就一直无法调用next函数进入真实回测阶段。

更新已处理K线长度

下面再来分析下bt中,依据K线数据时间更新len(self.data[x])的逻辑。

调用堆栈如下:

1. run, cerebro.py: 11272. runstrategies, cerebro.py: 12933. _runonce, cerebro.py: 1664

_runonce中相关代码如下:

def _runonce(self, runstrats):...while True:# Check next incoming date in the datasdts = [d.advance_peek() for d in datas]dt0 = min(dts)if dt0 == float('inf'):break # no data delivers anything# Timemaster if needed be# dmaster = datas[dts.index(dt0)] # and timemasterslen = len(runstrats[0])for i, dti in enumerate(dts):if dti <= dt0:datas[i].advance()# self._plotfillers2[i].append(slen) # mark as fillelse:# self._plotfillers[i].append(slen)pass...for strat in runstrats:strat._oncepost(dt0)...

dts = [d.advance_peek() for d in datas]返回的是1个日期的列表,每个元素是每个数据将要处理的日期。案例2中,加载了数据600035和300412,在首次循环时dts=[-01-08, -02-08](默认为时间戳,这里为了方面说明,转化为日期)。dt0取dts中的最小值,即-01-08。循环for i, dti in enumerate(dts)中,对待处理日期小于等于dt0的数据,进行 datas[i].advance(),而在advance函数中,进行了self.lencount += size,其中size默认为1。在len(self.datas[x])中,最底层也是访问的self.lencount。因此advance的调用就会改变len(self.datas[x])的值。len函数的底层方法实现如下:

def __len__(self):return self.lencount

对于案例2,600035的dti(-01-08) <= dt0(-01-08),因此会调用datas[i].advance();而300412的dti(-02-08)> dt0(-01-08),不会进行advance,因此没有改变len(self.datas[x]),这就导致上面提到的计算最小周期状态值minperstatus时,len(self.datas[x])一直为0,进而minperstatus=max(self._minperiods - map(len, self.datas)) > 0,进而无法进入next回测阶段。进入下一轮while循环 下一个待处理的日期列表dts = [-01-09, -02-08],即600035移动了1根K线数据,300412没有改变;dt0 = -01-09;循环for i, dti in enumerate(dts)中,600035的dti(-01-09) <= dt0(-01-09),因此会调用datas[i].advance();而300412的dti(-02-08)> dt0(-01-09),不会进行advance;最小周期状态值minperstatus > 0,未进入next。 进入下一轮while循环 下一个待处理的日期列表dts = [-01-10, -02-08],即600035移动了1根K线数据,300412没有改变;dt0 = -01-10;循环for i, dti in enumerate(dts)中,600035的dti(-01-10) <= dt0(-01-10),因此会调用datas[i].advance();而300412的dti(-02-08)> dt0(-01-10),不会进行advance;最小周期状态值minperstatus > 0,未进入next。 。。。。。。进入下一轮while循环 下一个待处理的日期列表dts = [-02-08, -02-08],即600035移动了1根K线数据,300412没有改变;dt0 = -02-08;循环for i, dti in enumerate(dts)中,600035的dti(-02-08) <= dt0(-02-08),因此会调用datas[i].advance();300412的dti(-02-08) <= dt0(-02-08),因此会调用datas[i].advance();最小周期状态值minperstatus = 0,进入next,开始回测。

通过上面的跟踪分析发现,虽然600035自回测开始时间-01-08就有K线数据,但是300412直到-02-08才有K线数据,回测直到两只股票都有K线数据时才会真正开始。也就是上面提到的,多股回测时,回测真实的开始时间是参与回测的所有股票,在设置的回测开始时间后,均具有最小周期个K线数据的时间

欢迎大家关注、点赞、转发、留言,感谢支持!

微信群用于学习交流,群1已满,群2已创建,感兴趣的读者请扫码加微信!

QQ群(676186743)用于资料共享,欢迎加入!

如果觉得《Python量化交易学习笔记(58)——backtrader多股回测的开始时间》对你有帮助,请点赞、收藏,并留下你的观点哦!

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。