内存优化#
1.3.1.92 版本 重新设计和完全实现了之前采用的内存优化方案,尽管没有太多宣传,也很少被使用。
backtrader
在开发过程中(现在也是如此)使用具有大量内存的计算机进行开发,而且结合了通过绘图提供的可视化反馈是一个不错的选择,几乎是必需的,所以选择将所有内容保存在内存中。
这个决策有一些缺点:
用于数据存储的
array.array
在超出某些界限时需要分配和移动数据内存较少的机器可能会受到影响
连接到实时数据源的情况下,该源可能在线运行数周/月,将以千秒/分钟的分辨率向系统输入大量的 Tick 数据
后一点比第一点更重要,因为在设计时另作了一个决策:
使用纯 Python 编写,以允许在嵌入式系统中运行(如果需要)
在未来的场景中,
backtrader
可能连接到第二台提供实时数据源的机器上,而backtrader
本身运行在树莓派或者更受限制的设备上,比如 ADSL 路由器 (AVM Frit!Box 7490,使用 Freetz 镜像) 上。因此,有必要使backtrader
支持动态内存方案。现在,可以使用以下语法来实例化或运行Cerebro
:
exactbars(默认值:False)
默认值为
False
,每个数值在内存中都会保留一行可能的值: -
True
或1
:所有“lines”对象将减少内存使用量,使其自动计算的最小周期。如果简单移动平均线的周期为30,底层数据将始终具有一个持续的30个柱形的缓冲区,以便计算简单移动平均线。
此设置将停用
preload
和runonce
使用此设置还会停用 绘图
-1
:策略级别的数据和指标/操作将保持所有数据在内存中。例如:
RSI
在内部使用指标UpDay
进行计算。此子指标不会将所有数据保留在内存中。这允许保持
plotting
和preloading
处于活动状态。runonce
将被停用--2
: 数据和指标作为策略的属性保留,将所有数据保存在内存中。
例如: RSI
内部使用指标 UpDay
进行计算。这个子指标将不会将所有数据保存在内存中。
如果在 __init__
中定义了类似 a = self.data.close - self.data.high
的语句,则 a
将不会将所有数据保存在内存中。
这样可以保持
plotting
和preloading
处于激活状态。runonce
将被禁用
和往常一样,一个例子胜过千言万语。下面是一个示例脚本展示了差异。它对从1996年到2015年的雅虎日线数据运行,总共4965天。
注意
这只是一个小样本。欧洲斯托克50期货每天交易14个小时,仅在一个月内就能产生大约18000个1分钟的K线。
首先执行脚本,以查看在没有请求内存节省时使用了多少内存位置:
`
$ ./memory-savings.py --save 0
使用的总内存单元数:506430
`
天哪!!!从 五十万 下降到 2041
。确实。系统中的每一个行对象都使用 collections.deque
作为缓冲区
(而不是 array.array
),并且长度被限制为所需操作的绝对最小值。例如:
使用“30”期间的
SimpleMovingAverage
策略对数据流进行处理。
在这种情况下,将进行以下调整:
数据流将具有“30”个位置的缓冲区,这是
SimpleMovingAverage
生成下一个值所需的数量
SimpleMovingAverage
将具有“1”个位置的缓冲区,因为除非其他*指标*(依靠移动平均)需要它, 否则不需要保留更大的缓冲区。
还有其他导致内存消耗累积的来源,比如策略生成的
orders
。这种模式只能在
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()