BackTrader 中文文档(十三)(4)

简介: BackTrader 中文文档(十三)

BackTrader 中文文档(十三)(3)https://developer.aliyun.com/article/1505346

在默认批处理runonce模式下执行挑战

我们的测试脚本(请查看底部获取完整源代码)将打开这 100 个文件,并使用默认的backtrader配置处理它们。

$ ./two-million-candles.py
Cerebro Start Time:          2019-10-26 08:33:15.563088
Strat Init Time:             2019-10-26 08:34:31.845349
Time Loading Data Feeds:     76.28
Number of data feeds:        100
Strat Start Time:            2019-10-26 08:34:31.864349
Pre-Next Start Time:         2019-10-26 08:34:32.670352
Time Calculating Indicators: 0.81
Next Start Time:             2019-10-26 08:34:32.671351
Strat warm-up period Time:   0.00
Time to Strat Next Logic:    77.11
End Time:                    2019-10-26 08:35:31.493349
Time in Strategy Next Logic: 58.82
Total Time in Strategy:      58.82
Total Time:                  135.93
Length of data feeds:        20000

内存使用:观察到峰值为 348 M 字节

大部分时间实际上是用于预加载数据(98.63 秒),其余时间用于策略,其中包括在每次迭代中通过经纪人(73.63 秒)。总时间为173.26秒。

根据您想如何计算,性能为:

  • 考虑整个运行时间为每秒14,713根蜡烛

底线:在上面两个 Reddit 帖子中声称backtrader无法处理 1.6M 根蜡烛的说法是错误的

使用pypy进行操作

既然该帖子声称使用pypy没有帮助,那么我们来看看使用它会发生什么。

$ ./two-million-candles.py
Cerebro Start Time:          2019-10-26 08:39:42.958689
Strat Init Time:             2019-10-26 08:40:31.260691
Time Loading Data Feeds:     48.30
Number of data feeds:        100
Strat Start Time:            2019-10-26 08:40:31.338692
Pre-Next Start Time:         2019-10-26 08:40:31.612688
Time Calculating Indicators: 0.27
Next Start Time:             2019-10-26 08:40:31.612688
Strat warm-up period Time:   0.00
Time to Strat Next Logic:    48.65
End Time:                    2019-10-26 08:40:40.150689
Time in Strategy Next Logic: 8.54
Total Time in Strategy:      8.54
Total Time:                  57.19
Length of data feeds:        20000

天啊!总时间从135.93秒降至57.19秒。性能翻了一番

性能:每秒34,971根蜡烛

内存使用:峰值为 269 兆字节。

这也是与标准 CPython 解释器相比的重要改进。

处理 2 百万根蜡烛超出核心内存

所有这些都可以改进,如果考虑到backtrader有几个配置选项用于执行回测会话,包括优化缓冲区并仅使用最少需要的数据集(理想情况下仅使用大小为1的缓冲区,这仅在理想情况下发生)

要使用的选项将是exactbars=True。从exactbars的文档中(这是在实例化Cerebro时或在调用run时传递给Cerebro的参数)

`True` or `1`: all “lines” objects reduce memory usage to the
  automatically calculated minimum period.
  If a Simple Moving Average has a period of 30, the underlying data
  will have always a running buffer of 30 bars to allow the
  calculation of the Simple Moving Average
  * This setting will deactivate `preload` and `runonce`
  * Using this setting also deactivates **plotting**

为了最大限度地优化,并且因为绘图将被禁用,以下内容也将被使用:stdstats=False,它禁用了用于现金、价值和交易的标准观察者(对绘图有用,但不再在范围内)

$ ./two-million-candles.py --cerebro exactbars=False,stdstats=False
Cerebro Start Time:          2019-10-26 08:37:08.014348
Strat Init Time:             2019-10-26 08:38:21.850392
Time Loading Data Feeds:     73.84
Number of data feeds:        100
Strat Start Time:            2019-10-26 08:38:21.851394
Pre-Next Start Time:         2019-10-26 08:38:21.857393
Time Calculating Indicators: 0.01
Next Start Time:             2019-10-26 08:38:21.857393
Strat warm-up period Time:   0.00
Time to Strat Next Logic:    73.84
End Time:                    2019-10-26 08:39:02.334936
Time in Strategy Next Logic: 40.48
Total Time in Strategy:      40.48
Total Time:                  114.32
Length of data feeds:        20000

性能:每秒17,494根蜡烛

内存使用:75 兆字节(在回测会话的整个过程中保持稳定)

让我们与之前未经优化的运行进行比较

  • 而不是花费超过76秒预加载数据,因为数据没有预加载,回测立即开始
  • 总时间为114.32秒,比135.93秒快了15.90%
  • 内存使用改进了68.5%

注意

实际上,我们可以向脚本输入 1 亿根蜡烛,内存消耗量仍将保持在75 兆字节不变

再次使用pypy进行操作

现在我们知道如何优化,让我们按照pypy的方式来做。

$ ./two-million-candles.py --cerebro exactbars=True,stdstats=False
Cerebro Start Time:          2019-10-26 08:44:32.309689
Strat Init Time:             2019-10-26 08:44:32.406689
Time Loading Data Feeds:     0.10
Number of data feeds:        100
Strat Start Time:            2019-10-26 08:44:32.409689
Pre-Next Start Time:         2019-10-26 08:44:32.451689
Time Calculating Indicators: 0.04
Next Start Time:             2019-10-26 08:44:32.451689
Strat warm-up period Time:   0.00
Time to Strat Next Logic:    0.14
End Time:                    2019-10-26 08:45:38.918693
Time in Strategy Next Logic: 66.47
Total Time in Strategy:      66.47
Total Time:                  66.61
Length of data feeds:        20000

性能:每秒30,025根蜡烛

内存使用:恒定为49 兆字节

与之前的等效运行相比:

  • 运行时间为66.61秒,比之前的114.32秒快了41.73%
  • 49 兆字节75 兆字节相比,内存使用改善了34.6%

注意

在这种情况下,pypy无法击败其批处理(runonce)模式的时间,即57.19秒。这是可以预料的,因为在预加载时,计算器指示是以向量化模式进行的,而这正是pypy的 JIT 擅长的地方。

无论如何,它仍然表现出色,并且在内存消耗方面有重要的改进

运行完整的交易

该脚本可以创建指标(移动平均线)并在 100 个数据源上执行多空策略,使用移动平均线的交叉。让我们用pypy来做,并且知道它与批处理模式更好,就这么办。

$ ./two-million-candles.py --strat indicators=True,trade=True
Cerebro Start Time:          2019-10-26 08:57:36.114415
Strat Init Time:             2019-10-26 08:58:25.569448
Time Loading Data Feeds:     49.46
Number of data feeds:        100
Total indicators:            300
Moving Average to be used:   SMA
Indicators period 1:         10
Indicators period 2:         50
Strat Start Time:            2019-10-26 08:58:26.230445
Pre-Next Start Time:         2019-10-26 08:58:40.850447
Time Calculating Indicators: 14.62
Next Start Time:             2019-10-26 08:58:41.005446
Strat warm-up period Time:   0.15
Time to Strat Next Logic:    64.89
End Time:                    2019-10-26 09:00:13.057955
Time in Strategy Next Logic: 92.05
Total Time in Strategy:      92.21
Total Time:                  156.94
Length of data feeds:        20000

性能:每秒12,743根蜡烛

内存使用:观察到峰值为1300 Mbytes

执行时间显然增加了(指标 + 交易),但为什么内存使用量增加了呢?

在得出任何结论之前,让我们运行它创建指标,但不进行交易

$ ./two-million-candles.py --strat indicators=True
Cerebro Start Time:          2019-10-26 09:05:55.967969
Strat Init Time:             2019-10-26 09:06:44.072969
Time Loading Data Feeds:     48.10
Number of data feeds:        100
Total indicators:            300
Moving Average to be used:   SMA
Indicators period 1:         10
Indicators period 2:         50
Strat Start Time:            2019-10-26 09:06:44.779971
Pre-Next Start Time:         2019-10-26 09:06:59.208969
Time Calculating Indicators: 14.43
Next Start Time:             2019-10-26 09:06:59.360969
Strat warm-up period Time:   0.15
Time to Strat Next Logic:    63.39
End Time:                    2019-10-26 09:07:09.151838
Time in Strategy Next Logic: 9.79
Total Time in Strategy:      9.94
Total Time:                  73.18
Length of data feeds:        20000

性能:27,329根蜡烛/秒

内存使用600 Mbytes(在优化的exactbars模式下进行相同操作仅消耗60 Mbytes,但执行时间增加,因为pypy本身不能进行如此大的优化)

有了这个:交易时内存使用量确实增加了。原因是OrderTrade对象被创建、传递并由经纪人保留。

注意

要考虑到数据集包含随机值,这会产生大量的交叉,因此会产生大量的订单和交易。不应期望常规数据集有类似的行为。

结论

无效声明

如上所证明的那样是虚假的,因为backtrader 能够处理 160 万根蜡烛以上。

一般情况

  1. backtrader可以轻松处理2M根蜡烛,使用默认配置(内存数据预加载)
  2. backtrader可以在非预加载优化模式下运行,将缓冲区减少到最小,以进行核心外存内存回测
  3. 当在优化的非预加载模式下进行回测时,内存消耗的增加来自经纪人生成的行政开销。
  4. 即使在交易时,使用指标并且经纪人不断介入,性能也是12,473根蜡烛/秒
  5. 在可能的情况下使用pypy(例如,如果你不需要绘图)

对于这些情况使用 Python 和/或backtrader

使用pypy,启用交易,并且使用随机数据集(比平常更多的交易),整个 2M 根蜡烛的处理时间为:

  • 156.94秒,即:几乎2 分钟 37 秒

考虑到这是在一台同时运行多个其他任务的笔记本电脑上完成的,可以得出结论,可以处理2M个条形图。

8000支股票的情况呢?

执行时间必须乘以 80,因此:

  • 需要运行这个随机集场景的时间为12,560秒(或几乎210 分钟3 小时 30 分钟)。

即使假设标准数据集会生成远少于操作,也仍然需要谈论几小时3 或 4)的回测时间

内存使用量也会增加,当交易时由于经纪人的操作,并且可能需要一些吉字节。

注意

这里不能简单地再次乘以 80,因为示例脚本使用随机数据进行交易,并尽可能频繁。无论如何,所需的 RAM 量都将是重要的

因此,仅使用backtrader作为研究和回测工具的工作流似乎有些牵强。

关于工作流的讨论

使用backtrader时需要考虑两种标准工作流程

  • 一切都用backtrader完成,即:研究和回测都在一个工具中完成
  • 使用 pandas 进行研究,获取想法是否良好的概念,然后使用 backtrader 进行回测,尽可能准确地验证,可能已将大型数据集缩减为对于常规 RAM 场景更易处理的内容。

提示

人们可以想象使用类似 dask 进行外存内存执行来替换 pandas

测试脚本

这里是源代码

#!/usr/bin/env python
# -*- coding: utf-8; py-indent-offset:4 -*-
###############################################################################
import argparse
import datetime
import backtrader as bt
class St(bt.Strategy):
    params = dict(
        indicators=False,
        indperiod1=10,
        indperiod2=50,
        indicator=bt.ind.SMA,
        trade=False,
    )
    def __init__(self):
        self.dtinit = datetime.datetime.now()
        print('Strat Init Time: {}'.format(self.dtinit))
        loaddata = (self.dtinit - self.env.dtcerebro).total_seconds()
        print('Time Loading Data Feeds: {:.2f}'.format(loaddata))
        print('Number of data feeds: {}'.format(len(self.datas)))
        if self.p.indicators:
            total_ind = self.p.indicators * 3 * len(self.datas)
            print('Total indicators: {}'.format(total_ind))
            indname = self.p.indicator.__name__
            print('Moving Average to be used: {}'.format(indname))
            print('Indicators period 1: {}'.format(self.p.indperiod1))
            print('Indicators period 2: {}'.format(self.p.indperiod2))
            self.macross = {}
            for d in self.datas:
                ma1 = self.p.indicator(d, period=self.p.indperiod1)
                ma2 = self.p.indicator(d, period=self.p.indperiod2)
                self.macross[d] = bt.ind.CrossOver(ma1, ma2)
    def start(self):
        self.dtstart = datetime.datetime.now()
        print('Strat Start Time: {}'.format(self.dtstart))
    def prenext(self):
        if len(self.data0) == 1:  # only 1st time
            self.dtprenext = datetime.datetime.now()
            print('Pre-Next Start Time: {}'.format(self.dtprenext))
            indcalc = (self.dtprenext - self.dtstart).total_seconds()
            print('Time Calculating Indicators: {:.2f}'.format(indcalc))
    def nextstart(self):
        if len(self.data0) == 1:  # there was no prenext
            self.dtprenext = datetime.datetime.now()
            print('Pre-Next Start Time: {}'.format(self.dtprenext))
            indcalc = (self.dtprenext - self.dtstart).total_seconds()
            print('Time Calculating Indicators: {:.2f}'.format(indcalc))
        self.dtnextstart = datetime.datetime.now()
        print('Next Start Time: {}'.format(self.dtnextstart))
        warmup = (self.dtnextstart - self.dtprenext).total_seconds()
        print('Strat warm-up period Time: {:.2f}'.format(warmup))
        nextstart = (self.dtnextstart - self.env.dtcerebro).total_seconds()
        print('Time to Strat Next Logic: {:.2f}'.format(nextstart))
        self.next()
    def next(self):
        if not self.p.trade:
            return
        for d, macross in self.macross.items():
            if macross > 0:
                self.order_target_size(data=d, target=1)
            elif macross < 0:
                self.order_target_size(data=d, target=-1)
    def stop(self):
        dtstop = datetime.datetime.now()
        print('End Time: {}'.format(dtstop))
        nexttime = (dtstop - self.dtnextstart).total_seconds()
        print('Time in Strategy Next Logic: {:.2f}'.format(nexttime))
        strattime = (dtstop - self.dtprenext).total_seconds()
        print('Total Time in Strategy: {:.2f}'.format(strattime))
        totaltime = (dtstop - self.env.dtcerebro).total_seconds()
        print('Total Time: {:.2f}'.format(totaltime))
        print('Length of data feeds: {}'.format(len(self.data)))
def run(args=None):
    args = parse_args(args)
    cerebro = bt.Cerebro()
    datakwargs = dict(timeframe=bt.TimeFrame.Minutes, compression=15)
    for i in range(args.numfiles):
        dataname = 'candles{:02d}.csv'.format(i)
        data = bt.feeds.GenericCSVData(dataname=dataname, **datakwargs)
        cerebro.adddata(data)
    cerebro.addstrategy(St, **eval('dict(' + args.strat + ')'))
    cerebro.dtcerebro = dt0 = datetime.datetime.now()
    print('Cerebro Start Time: {}'.format(dt0))
    cerebro.run(**eval('dict(' + args.cerebro + ')'))
def parse_args(pargs=None):
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description=(
            'Backtrader Basic Script'
        )
    )
    parser.add_argument('--numfiles', required=False, default=100, type=int,
                        help='Number of files to rea')
    parser.add_argument('--cerebro', required=False, default='',
                        metavar='kwargs', help='kwargs in key=value format')
    parser.add_argument('--strat', '--strategy', required=False, default='',
                        metavar='kwargs', help='kwargs in key=value format')
    return parser.parse_args(pargs)
if __name__ == '__main__':
    run()

交叉回测陷阱

原文:www.backtrader.com/blog/posts/2019-09-04-donchian-across-platforms/donchian-across-platforms/

backtrader 社区 中经常出现的一件事是,用户解释了希望复制在例如 TradingView 中获得的回测结果,这在当今非常流行,或者其他一些回测平台。

即使不真正了解 TradingView 中使用的语言 Pinescript,并且对回测引擎的内部没有任何了解,仍然有一种方法可以让用户知道,跨平台编码必须谨慎对待。

指标:并非始终忠实于来源

当为 backtrader 实现新的指标时,无论是直接用于分发还是作为网站的片段,都会非常强调尊重原始定义。 RSI 就是一个很好的例子。

  • 韦尔斯·怀尔德设计 RSI 时使用的是 Modified Moving Average(又称 Smoothed Moving Average,参见 Wikipedia - Modified Moving Average )
  • 尽管如此,许多平台给用户提供了所谓的 RSI,但使用的是经典的 指数移动平均线 而不是书中所说的。
  • 鉴于这两个平均值都是指数型的,差异并不是很大,但这并不是韦尔斯·怀尔德定义的。它可能仍然有用,甚至可能更好,但这不是 RSI。而且文档(如果有的话)也没有提到这一点。

backtraderRSI 的默认配置是使用 MMA 以忠实于来源,但要使用哪种移动平均线是可以通过子类化或在运行时实例化期间更改的参数,以使用 EMA 或甚至 简单移动平均线

一个例子:唐奇安通道

维基百科的定义:维基百科 - 唐奇安通道 ). 这只是文本,没有提到使用通道突破作为交易信号。

另外两个定义:

这两个参考资料明确指出,用于计算通道的数据不包括当前柱,因为如果包括…突破将不会反映。这里是 StockCharts 的一个示例图表

现在转向 TradingView。首先是链接

该页面上的一个图表。

即使Investopedia也使用了一张TradingView图表,显示没有突破。这里:Investopedia - 唐奇安通道 - https://www.investopedia.com/terms/d/donchianchannels.asp

正如一些人所说… 天啊!!! 因为TradingView的图表中没有突破可见。这意味着指标的实现是使用当前价格栏来计算通道。

backtrader中的唐奇安通道

标准backtrader发行版中没有DonchianChannels的实现,但可以很快制作。一个参数将决定当前栏是否用于通道计算。

class DonchianChannels(bt.Indicator):
  '''
 Params Note:
 - ``lookback`` (default: -1)
 If `-1`, the bars to consider will start 1 bar in the past and the
 current high/low may break through the channel.
 If `0`, the current prices will be considered for the Donchian
 Channel. This means that the price will **NEVER** break through the
 upper/lower channel bands.
 '''
    alias = ('DCH', 'DonchianChannel',)
    lines = ('dcm', 'dch', 'dcl',)  # dc middle, dc high, dc low
    params = dict(
        period=20,
        lookback=-1,  # consider current bar or not
    )
    plotinfo = dict(subplot=False)  # plot along with data
    plotlines = dict(
        dcm=dict(ls='--'),  # dashed line
        dch=dict(_samecolor=True),  # use same color as prev line (dcm)
        dcl=dict(_samecolor=True),  # use same color as prev line (dch)
    )
    def __init__(self):
        hi, lo = self.data.high, self.data.low
        if self.p.lookback:  # move backwards as needed
            hi, lo = hi(self.p.lookback), lo(self.p.lookback)
        self.l.dch = bt.ind.Highest(hi, period=self.p.period)
        self.l.dcl = bt.ind.Lowest(lo, period=self.p.period)
        self.l.dcm = (self.l.dch + self.l.dcl) / 2.0  # avg of the above

使用lookback=-1参数,一个示例图表看起来像这样(放大后)

人们可以清楚地看到突破,而在lookback=0版本中没有突破。

编码影响

程序员首先去商业平台,并使用唐奇安通道实现一个策略。因为图表上没有显示突破,所以必须将当前价格值与前一个通道值进行比较。如下所示

if price0 > channel_high_1:
    sell()
elif price0 < channel_low_1:
    buy()

当前价格,即:price01周期前的高/低通道值进行比较(因此有_1后缀)

作为一个谨慎的程序员,不知道backtrader唐奇安通道的默认设置是有突破的,代码被移植过来,如下所示

def __init__(self):
        self.donchian = DonchianChannels()
    def next(self):
        if self.data[0] > self.donchian.dch[-1]:
            self.sell()
        elif self.data[0] < self.donchian.dcl[-1]:
            self.buy()

这是错误的!!!因为突破发生在比较的同时。正确的代码:

def __init__(self):
        self.donchian = DonchianChannels()
    def next(self):
        if self.data[0] > self.donchian.dch[0]:
            self.sell()
        elif self.data[0] < self.donchian.dcl[0]:
            self.buy()

虽然这只是一个小例子,但它展示了由于指标被编码为1栏差异而导致的回测结果可能会有所不同。这看起来可能并不多,但当错误的交易开始时,它肯定会产生影响。

相关文章
|
Unix 索引 Python
BackTrader 中文文档(一)(2)
BackTrader 中文文档(一)
432 0
|
存储 编解码 算法
BackTrader 中文文档(十一)(3)
BackTrader 中文文档(十一)
302 0
|
存储 缓存 Shell
BackTrader 中文文档(二)(3)
BackTrader 中文文档(二)
380 0
|
索引
BackTrader 中文文档(九)(2)
BackTrader 中文文档(九)
301 0
|
测试技术
BackTrader 中文文档(九)(1)
BackTrader 中文文档(九)
161 0
BackTrader 中文文档(二十九)(3)
BackTrader 中文文档(二十九)
157 0
|
编解码 C++ 索引
BackTrader 中文文档(九)(3)
BackTrader 中文文档(九)
387 0
|
存储 数据库连接 数据库
BackTrader 中文文档(三)(2)
BackTrader 中文文档(三)
258 0
|
机器学习/深度学习 算法 机器人
BackTrader 中文文档(一)(1)
BackTrader 中文文档(一)
953 0
|
存储 API 索引
BackTrader 中文文档(十一)(2)
BackTrader 中文文档(十一)
263 0