You are here:  Home » 量化交易与机器学习 » backtrader » 如何提高backtrader回测性能1倍以上且优化内存- backtrader中文的教程

使用200万条K线的数据,测试backtrader的回测性能如何?

为​​了做到这一点,第一件事就是产生的足够的K线。所以,我们会做以下动作:

  • 产生100支股票
  • 每支股票 20000条K线数据

100个股票数据文件总计200万 根K线数据.

代码:

import numpy as np
import pandas as pd

COLUMNS = ['open', 'high', 'low', 'close', 'volume', 'openinterest']
CANDLES = 20000
STOCKS

dateindex = pd.date_range(start='2010-01-01', periods=CANDLES, freq='15min')

for i in range(STOCKS):

    data = np.random.randint(10, 20, size=(CANDLES, len(COLUMNS)))
    df = pd.DataFrame(data * 1.01, dateindex, columns=COLUMNS)
    df = df.rename_axis('datetime')
    df.to_csv('candles{:02d}.csv'.format(i))

 

这会生成 100 个文件,从candles00.csv到candles99.csv. 其中实际值并不重要。拥有标准 datetimeOHLCV(和OpenInterest)才是最重要的。

测试系统

  • 硬件/操作系统:将使用配备 Intel i7 和 32 GB 内存的Windows 10的 15.6″笔记本电脑。
  • Python : CPython3.6.1pypy3 6.0.0
  • 其他:持续运行并占用大约 20% 的 CPU 的应用程序。正在运行着Chrome(102 个进程)、Edge、Word、Powerpoint、Excel 和一些小型应用程序等通常的程序。

默认配置

让我们回顾一下backtrader的默认运行时配置是什么:

  • 如果可能,预加载所有数据馈送
  • 如果可以预加载所有数据馈送,则以批处理模式运行(命名为runonce
  • 首先预先计算所有指标
  • 逐步了解策略逻辑和经纪人

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 MB 的峰值

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

根据您想要计算它的方式,性能是:

  • 考虑到整个运行时间:14,713根K线/秒

说明以这样的数据量backtrader处理起来,基本没有压力,内存的处理上,还可以通过参数的设置进行优化。将在后面做更多的探索。

比较使用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根K线/秒

内存使用:观察到 269 MB 的峰值。

这也是对标准 CPython 解释器的重要改进。

 

Handling 2M的蜡烛出核心memory

如果考虑到backtrader有多个用于执行回测会话的配置选项,所有这些都可以得到改进,包括优化缓冲区和仅使用所需的最少数据集(理想情况下仅使用 size 的缓冲区,这只会发生在理想场景)

class backtrader.Cerebro()
参数:

preload(默认True:)
是否预加载data feeds传递给 cerebro

runonce(默认:True)
以矢量化模式运行Indicators以加速整个系统。策略和观察者将始终基于事件运行

live(默认:False)
默认是回测数据。

当使用实时数据时设置成True(或通过数据的islive 方法)

这将同时停用preload和runonce。它对内存节省方案没有影响。

以矢量化模式运行Indicators以加速整个系统。策略和观察者将始终基于事件运行

maxcpus(默认值:None -> 所有可用内核)
同时使用多少个内核进行优化

stdstats(默认:True)
默认将添加真正的默认观察员:经纪人(现金和价值)、交易和买入卖出

oldbuysell(默认:False)(与画图相关)
如果stdstatsis:True 时观察者自动添加,则此开关使用BuySell

False:其中买入/卖出信号分别绘制在低/高价下方/上方,以避免混乱

True:在该行为中绘制买入/卖出信号在给定时间的订单执行的平均价格。这当然会在 OHLC 条的顶部或在 Close 的 Line 上,从而难以识别。

oldtrades(默认:False)(与画图相关)
如果stdstatsis:True时观察者自动添加,则此开关控制Trades 

False:其中所有数据的交易都用不同的标记绘制

True:同一方向的交易用相同的标记绘制交易,仅区分它们是正数还是负数

exactbars(默认:False)
使用默认值,存储在一行中的每个值都保存在内存中

`True` 或 `1`:所有“行”对象将内存使用量减少到自动计算的最小周期。

  如果简单移动平均线的周期为 30,则基础数据将始终具有 30 个柱的运行缓冲区,以允许计算简单移动平均线

  * 此设置将停用 `preload` 和 `runonce` 

  * 使用此设置也会停用**绘图** 

objcache (default: False)
如果为True实现line对象的缓存。

writer(默认: False)
如果设置为True时 它将标准信息的输出生成一个默认文件

tradehistory(默认: False)
如果设置为True,它将在所有策略的每笔交易中激活更新事件记录log。这也可以在每个策略的上使用set_tradehistory来实现

optdatas(默认:True)
如果True优化(并且preload和runonce也是True),数据预加载将在主进程中只进行一次,以节省时间和资源。

optreturn(默认:True)
如果True优化结果只有params属性和analyzers指标,而不是完整Strategy 对象(以及所有数据、指标、观察者……),这样可以优化速度,测试显示改善13% - 15%的执行时间

oldsync(默认False:)
从版本 1.9.0.99 开始,多个数据(相同或不同时间范围)的同步已更改为允许不同长度的数据。

如果希望使用 data0 作为系统主控的旧行为,请将此参数设置为 true

tz(默认:None)
为策略添加全球时区。论据tz可以是

* `None`:在这种情况下,策略显示的日期时间将采用UTC,这是标准行为

* `pytz` 实例。它将用于将 UTC 时间转换为所选时区

* `string`。将尝试实例化 `pytz` 实例。

* `整数`。
  对于策略,使用与 `self.datas` 迭代中相应的 `data`相同的时区(`0` 将使用来自 `data0` 的时区)

cheat_on_open(默认:False)
当为True时next_open调用发生在next方法调用之前。此时指标尚未重新计算。这允许发布一个考虑前一天指标但使用open价格计算的订单

对于 cheat_on_open 订单执行,还需要调用cerebro.broker.set_coo(True)或实例化一个经纪人 BackBroker(coo=True)(其中coo代表 cheat-on-open)或将broker_coo参数设置为True. 除非在下面禁用,否则 Cerebro 会自动执行此操作。

broker_coo(默认:True)
这将自动调用set_coo代理的方法True来激活cheat_on_open执行。cheat_on_open要同时为True

quicknotify(默认:False)
经纪人通知在下一个价格交付之前交付 。对于回溯测试,这没有任何影响,但是对于实时经纪人,可以在柱线交付之前很久就发出通知。设置为True通知将尽快发送(请参阅qcheck实时提要)

设置False为兼容性。可以改为True

 

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

为了最大程度的优化并且禁用绘图,也将使用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根K线/秒

内存使用:75M字节(从开始回测开始到结束,稳定在这个数值)

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

  • 无需花费76秒钟预加载数据,而是立即开始回测。
  • 总时间是114.32秒 比 135.93秒改进15.90%
  • 使用内存改进了68.5%

再次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
时间加载数据馈送:0.10
数据馈送数量:100 
Strat 开始时间:2019-10-26 08:44:32.409689 
Pre-Next Start Time:2019-10-26 08:44:32.451689
时间计算指标:0.04 
Next Start Time:2019 -10-26 08:44:32.451689 战略
预热期时间:0.00战略下一个逻辑时间
:0.14
结束时间:2019-10-26 08:45:38.918693
战略下一个逻辑时间:66.47
战略总时间:66.47
总时间:66.61
数据馈送长度:20000

性能:30,025根K线/秒

内存使用:恒定在49 M字节

将其与之前运行进行比较:

  • 66.61秒 比114.32t秒,在运行时间上有41.73%的改进。
  • 49 M字节比75 M字节,在内存上有34.6%的改进。

在这种情况下,与批处理模式pypy相比,它无法击败自己的时间。这是意料之中的,因为在预加载时,计算器指示是在矢量化模式下完成的。

无论如何,它仍然做得非常好,并且内存消耗有了重要的改善

完整的交易运行

该脚本可以创建指标(移动平均线)并使用移动平均线的交叉短期/长期策略对 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根K线/秒

内存使用1300 M字节观察到一个峰值。

由于增加了指标和交易,执行时间明显增加了,但是为什么内存使用也增加了?

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

$ ./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 根K线/秒

内存使用:(600 M字节在优化exactbars模式下做同样的事情只会消耗60 M字节,但会增加执行时间,因为 pypy它本身不能优化这么多)

有了交易,内存使用量确实增加了。原因是对象是由代理创建、传递和保存的Order和Trade。

还有该数据集包含随机值,其产生数量庞大交叉的,因此有大量的订单和交易。对于常规数据集,不会有类似的行为。

结论

    1. backtrader可以使用默认配置轻松处理2M蜡烛图(预加载内存数据)
    2. backtrader可以在非预加载优化模式下运行,将缓冲区减少到最小,以进行减少内存使用进行回测
    3. 优化的非预加载模式下进行回测时,内存消耗的增加来自于代理产生的管理开销。
    4. 即使交易、使用指标和经纪人不断阻碍,表现也是12,473根K线/秒
    5. 尽可能使用pypy(如果您不需要绘图的时候)

测试脚本

这里是源代码

#!/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()

 

评论被关闭。