Skip to content

关于性能回溯测试和核外内存执行

原文: https://www.backtrader.com/blog/2019-10-25-on-backtesting-performance-and-out-of-memory/on-backtesting-performance-and-out-of-memory/

最近有两次https://redit.com/r/algotrading 这是本文的灵感所在。

** 还有一个要求能够对 8000 多只股票进行回溯测试:reddit/r/algotrading——回溯测试支持 1000 多只股票的 LIB?

 *作者询问了一个框架,该框架可以对“超出核心/内存”**“因为显然它无法将所有这些数据加载到内存”***

当然,我们将通过反向交易者来解决这些概念

两百万支蜡烛

为了做到这一点,第一件事是产生的蜡烛数量。考虑到第一张海报上说的是 77 支股票和 160 万支蜡烛,这相当于每支股票 20779 支蜡烛,所以我们将做下面的工作来获得好的数字

  • 制作 100 支蜡烛

  • 每支存货产生 20000 支蜡烛

即:100 个文件,共 200 万支蜡烛。

剧本

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 1015.6“笔记本电脑。

  • Python:CPython3.6.1pypy3 6.0.0

  • Misc:一个持续运行的应用程序,占用大约 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 

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

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

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

  • 考虑到整个运行时间,14,713烛光/秒

底线:上面两条 reddit 线中的backtrader无法处理 160 万支蜡烛的 1st中的索赔为

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 解释器的重要改进。

处理从核心内存中取出的内存

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

将使用的选项为exactbars=True。来自exactbars的文档(在实例化或调用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秒 vs135.93秒。15.90%的改进。

  • 68.5%内存使用的改进。

笔记

实际上,我们可以向脚本投掷 1 亿支蜡烛,消耗的内存量将保持固定在75 Mbytes

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 Mbytes

将其与之前的等效运行进行比较:

  • 66.61秒与114.3241.73%运行时间的改进

  • 49 Mbytesvs75 Mbytes34.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对象是由代理创建、传递和保存的。

笔记

考虑到数据集包含随机值,这会产生大量的交叉,从而产生大量的订单和交易。对于常规数据集,不应出现类似的行为。

结论

虚假主张

以上已经证明为,因为反向交易者可以处理 160 万支蜡烛和更多蜡烛。

全体的

  1. backtrader可以使用默认配置轻松处理2M蜡烛(带内存数据预加载)

  2. backtrader可以在非预加载优化模式下运行,将缓冲区减少到最小,以进行核心外内存回溯测试

  3. 在优化的非预加载模式下进行回溯测试时,内存消耗的增加来自代理产生的管理开销。

  4. 即使在交易、使用指标和经纪人不断阻挠的情况下,其表现也是12,473烛光/秒

  5. 尽可能使用pypy(例如,如果不需要绘图)

在这些情况下使用 Python 和/或backtrader

使用pypy,启用交易,并使用随机数据集(高于正常交易数量),总共处理了整个 2M 条:

  • 156.94秒,即:几乎2 minutes and 37 seconds

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

那么8000股票的情况呢?

执行时间必须按 80 调整,因此:

  • 运行此随机集场景需要12,560 seconds(或几乎210 minutes3 hours and 30 minutes)。

即使假设一个标准的数据集产生的操作要少得多,人们仍然会谈论在小时3 or 4小时)内的回溯测试

由于经纪人的行为,交易时,内存使用量也会增加,可能需要一些千兆字节。

笔记

这里不能简单地再乘以 80,因为示例脚本使用随机数据,并且尽可能频繁地进行交易。在任何情况下,所需的 RAM 量都是非常重要的

因此,一个只有backtrader作为研究和回溯测试工具的工作流似乎有些牵强。

关于工作流的讨论

有两个标准的工作流程需要考虑使用 TytT0.

  • 使用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() 

我们一直在努力

apachecn/AiLearning

【布客】中文翻译组