如何提高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
. 其中实际值并不重要。拥有标准 datetime
、OHLCV
(和OpenInterest
)才是最重要的。
测试系统
- 硬件/操作系统:将使用配备 Intel i7 和 32 GB 内存的Windows 10的 15.6″笔记本电脑。
- Python : CPython
3.6.1
和pypy3 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。
还有该数据集包含随机值,其产生数量庞大交叉的,因此有大量的订单和交易。对于常规数据集,不会有类似的行为。
结论
-
- backtrader可以使用默认配置轻松处理
2M
蜡烛图(预加载内存数据) - backtrader可以在非预加载优化模式下运行,将缓冲区减少到最小,以进行减少内存使用进行回测
- 在优化的非预加载模式下进行回测时,内存消耗的增加来自于代理产生的管理开销。
- 即使交易、使用指标和经纪人不断阻碍,表现也是
12,473
根K线/秒 - 尽可能使用
pypy
(如果您不需要绘图的时候)
- backtrader可以使用默认配置轻松处理
测试脚本
这里是源代码
#!/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()
评论被关闭。