内存优化#

1.3.1.92 版本 重新设计和完全实现了之前采用的内存优化方案,尽管没有太多宣传,也很少被使用。

backtrader 在开发过程中(现在也是如此)使用具有大量内存的计算机进行开发,而且结合了通过绘图提供的可视化反馈是一个不错的选择,几乎是必需的,所以选择将所有内容保存在内存中。

这个决策有一些缺点:

  • 用于数据存储的 array.array 在超出某些界限时需要分配和移动数据

  • 内存较少的机器可能会受到影响

  • 连接到实时数据源的情况下,该源可能在线运行数周/月,将以千秒/分钟的分辨率向系统输入大量的 Tick 数据

后一点比第一点更重要,因为在设计时另作了一个决策:

  • 使用纯 Python 编写,以允许在嵌入式系统中运行(如果需要)

    在未来的场景中, backtrader 可能连接到第二台提供实时数据源的机器上,而 backtrader 本身运行在树莓派或者更受限制的设备上,比如 ADSL 路由器 (AVM Frit!Box 7490,使用 Freetz 镜像) 上。因此,有必要使 backtrader 支持动态内存方案。现在,可以使用以下语法来实例化或运行 Cerebro

  • exactbars(默认值:False)

    • 默认值为 False ,每个数值在内存中都会保留一行

    • 可能的值: - True1 :所有“lines”对象将减少内存使用量,使其自动计算的最小周期。

      如果简单移动平均线的周期为30,底层数据将始终具有一个持续的30个柱形的缓冲区,以便计算简单移动平均线。

      • 此设置将停用 preloadrunonce

      • 使用此设置还会停用 绘图

      • -1 :策略级别的数据和指标/操作将保持所有数据在内存中。

        例如: RSI 在内部使用指标 UpDay 进行计算。此子指标不会将所有数据保留在内存中。

        • 这允许保持 plottingpreloading 处于活动状态。

        • runonce 将被停用- -2 : 数据和指标作为策略的属性保留,将所有数据保存在内存中。

例如: RSI 内部使用指标 UpDay 进行计算。这个子指标将不会将所有数据保存在内存中。

如果在 __init__ 中定义了类似 a = self.data.close - self.data.high 的语句,则 a 将不会将所有数据保存在内存中。

  • 这样可以保持 plottingpreloading 处于激活状态。

  • runonce 将被禁用

和往常一样,一个例子胜过千言万语。下面是一个示例脚本展示了差异。它对从1996年到2015年的雅虎日线数据运行,总共4965天。

注意

这只是一个小样本。欧洲斯托克50期货每天交易14个小时,仅在一个月内就能产生大约18000个1分钟的K线。

首先执行脚本,以查看在没有请求内存节省时使用了多少内存位置:

` $ ./memory-savings.py --save 0 使用的总内存单元数:506430 `

对于级别1(总共节省):```

$ ./memory-savings.py –save 1 使用的总内存单元:2041

天哪!!!从 五十万 下降到 2041 。确实。系统中的每一个行对象都使用 collections.deque 作为缓冲区 (而不是 array.array ),并且长度被限制为所需操作的绝对最小值。例如:

  • 使用“30”期间的 SimpleMovingAverage 策略对数据流进行处理。

在这种情况下,将进行以下调整:

  • 数据流将具有“30”个位置的缓冲区,这是 SimpleMovingAverage 生成下一个值所需的数量

  • SimpleMovingAverage 将具有“1”个位置的缓冲区,因为除非其他*指标*(依靠移动平均)需要它, 否则不需要保留更大的缓冲区。

注意

这种模式最有吸引力和可能最重要的特点是,使用的内存量在脚本的整个生命周期中保持恒定。

无论数据流的大小如何。

如果例如连续长时间连接到实时数据流,这将非常有用。

但请注意:1. *绘图*不可用

  1. 还有其他导致内存消耗累积的来源,比如策略生成的 orders

  2. 这种模式只能在 cerebro 中的 runonce=False 的情况下使用。这对于实时数据源来说是强制性的,但对于简单的回测来说,这比 runonce=True 要慢。

从某种程度上说,内存管理的成本比逐步执行回测更高,但只能由平台的最终用户根据具体情况来判断。

现在是负级别。这些级别旨在保持*绘图*可用的同时,仍然节省相当多的内存。第一个级别为 -1

$ ./memory-savings.py –save -1 使用的总内存单元:184623

在这种情况下,*指标*的第一级(在策略中声明的指标)保持其完整长度的缓冲区。但是,如果这些指标依赖于其他指标(就是这种情况),以完成它们的工作,子对象的长度将受限制。在这种情况下,我们从:

  • 506430 个内存位置 -> 184623 个内存位置

节省了50%以上。

注意

当然, array.array 对象已经被 collections.deque 对象所替代,后者在内存方面的成本更高,但在操作方面更快。但是 collections.deque 对象相当小,节省了大约计算过的内存位置。现在是 -2 级,这意味着还可以节省在策略级别声明的指标,并且已标记为不绘制的指标:

$ ./memory-savings.py –save -2 总内存单元使用量:174695

现在并没有节省多少。这是因为已标记一个指标不绘制: TestInd().plotinfo.plot = False

让我们看看最后一个例子的绘图结果:

$ ./memory-savings.py –save -2 –plot 总内存单元使用量:174695

对于感兴趣的读者,示例脚本可以对 指标*层级中遍历的每个 lines 对象进行详细分析。启用 绘图*功能(保存在 -1 位置)运行:

$ ./memory-savings.py –save -1 –lendetails – 评估数据 —- 数据 0 总单元数 34755 - 每行单元数 4965 – 评估指标 —- 指标 1.0 平均 总单元数 30 - 每行单元数 30 —- 子指标 总单元数 1 —- 指标 1.1 _LineDelay 总单元数 1 - 每行单元数 1 —- 子指标 总单元数 1 … —- 指标 0.5 TestInd 总单元数 9930 - 每行单元数 4965 —- 子指标 总单元数 0 – 评估观察器 —- 观察器 0 总单元数 9930 - 每行单元数 4965 —- 观察器 1 总单元数 9930 - 每行单元数 4965 —- 观察器 2 总单元数 9930 - 每行单元数 4965 总内存单元使用量:184623

使用最大节省(启用“1”)的情况下相同的操作:

$ ./memory-savings.py –save 1 –lendetails – 评估数据 —- 数据 0 总单元数 266 - 每行单元数 38 – 评估指标 —- 指标 1.0 平均 总单元数 30 - 每行单元数 30 —- 子指标 总单元数 1 … —- 指标 0.5 TestInd 总单元数 2 - 每行单元数 1 —- 子指标 总单元数 0 – 评估观察器 —- 观察器 0 总单元数 2 - 每行单元数 1 —- 观察器 1 总单元数 2 - 每行单元数 1 —- 观察器 2 总单元数 2 - 每行单元数 1第二个输出立即显示了 数据源 中的行数被限制在 38 个内存位置上,而不是完整数据源长度 4965

当可能时, 指标*和 观察器*被限制在 1 个内存位置上,如输出的最后几行所示。

脚本代码和用法#

backtrader 的示例代码中可用。用法:

$ ./memory-savings.py --help
usage: memory-savings.py [-h] [--data DATA] [--save SAVE] [--datalines]
                         [--lendetails] [--plot]

检查内存节省

可选参数:
  -h, --help    显示帮助信息并退出
  --data DATA   要读入的数据 (默认: ../../datas/yhoo-1996-2015.txt)
  --save SAVE   内存节省级别 [1, 0, -1, -2](默认: 0)
  --datalines   打印数据行(默认: False)
  --lendetails  打印各个项目的内存使用情况(默认: False)
  --plot        绘制结果图(默认: False)

代码:

from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import argparse
import sys

import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind
import backtrader.utils.flushfile


class TestInd(bt.Indicator):
    lines = ('a', 'b')

    def __init__(self):
        self.lines.a = b = self.data.close - self.data.high
        self.lines.b = btind.SMA(b, period=20)


class St(bt.Strategy):
    params = (
        ('datalines', False),
        ('lendetails', False),
    )

    def __init__(self):
        btind.SMA()
        btind.Stochastic()
        btind.RSI()
        btind.MACD()
        btind.CCI()
        TestInd().plotinfo.plot = False

    def next(self):
        if self.p.datalines:
            txt = ','.join(
                ['%04d' % len(self),
                 '%04d' % len(self.data0),
                 self.data.datetime.date(0).isoformat()]
            )

            print(txt)

    def loglendetails(self, msg):
        if self.p.lendetails:
            print(msg)

    def stop(self):
        super(St, self).stop()

        tlen = 0
        self.loglendetails('-- Evaluating Datas')
        for i, data in enumerate(self.datas):
            tdata = 0
            for line in data.lines:
                tdata += len(line.array)
                tline = len(line.array)

            tlen += tdata
            logtxt = '---- Data {} Total Cells {} - Cells per Line {}'
            self.loglendetails(logtxt.format(i, tdata, tline))

        self.loglendetails('-- Evaluating Indicators')
        for i, ind in enumerate(self.getindicators()):
            tlen += self.rindicator(ind, i, 0)

        self.loglendetails('-- Evaluating Observers')
        for i, obs in enumerate(self.getobservers()):
            tobs = 0
            for line in obs.lines:
                tobs += len(line.array)
                tline = len(line.array)

            tlen += tdata
            logtxt = '---- Observer {} Total Cells {} - Cells per Line {}'
            self.loglendetails(logtxt.format(i, tobs, tline))

        print('Total memory cells used: {}'.format(tlen))

    def rindicator(self, ind, i, deep):
        tind = 0
        for line in ind.lines:
            tind += len(line.array)
            tline = len(line.array)

        thisind = tind

        tsub = 0
        for j, sind in enumerate(ind.getindicators()):
            tsub += self.rindicator(sind, j, deep + 1)

        iname = ind.__class__.__name__.split('.')[-1]

        logtxt = '---- Indicator {}.{} {} Total Cells {} - Cells per line {}'
        self.loglendetails(logtxt.format(deep, i, iname, tind, tline))
        logtxt = '---- SubIndicators Total Cells {}'
        self.loglendetails(logtxt.format(deep, i, iname, tsub))

        return tind + tsub


def runstrat():
    args = parse_args()

    cerebro = bt.Cerebro()
    data = btfeeds.YahooFinanceCSVData(dataname=args.data)
    cerebro.adddata(data)
    cerebro.addstrategy(
        St, datalines=args.datalines, lendetails=args.lendetails)

    cerebro.run(runonce=False, exactbars=args.save)
    if args.plot:
        cerebro.plot(style='bar')


def parse_args():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        description='Check Memory Savings')

    parser.add_argument('--data', required=False,
                        default='../../datas/yhoo-1996-2015.txt',
                        help='Data to be read in')

    parser.add_argument('--save', required=False, type=int, default=0,
                        help=('Memory saving level [1, 0, -1, -2]'))

    parser.add_argument('--datalines', required=False, action='store_true',
                        help=('Print data lines'))

    parser.add_argument('--lendetails', required=False, action='store_true',
                        help=('Print individual items memory usage'))

    parser.add_argument('--plot', required=False, action='store_true',
                        help=('Plot the result'))

    return parser.parse_args()


if __name__ == '__main__':
    runstrat()