BackTrader 中文文档(二十四)(1)https://developer.aliyun.com/article/1505445
代码:
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime # The above could be sent to an independent module import backtrader as bt from backtrader.utils import flushfile # win32 quick stdout flushing class TestStrategy(bt.Strategy): params = dict( smaperiod=5, trade=False, stake=10, exectype=bt.Order.Market, stopafter=0, valid=None, cancel=0, donotsell=False, ) def __init__(self): # To control operation entries self.orderid = list() self.order = None self.counttostop = 0 self.datastatus = 0 # Create SMA on 2nd data self.sma = bt.indicators.MovAv.SMA(self.data, period=self.p.smaperiod) print('--------------------------------------------------') print('Strategy Created') print('--------------------------------------------------') def notify_data(self, data, status, *args, **kwargs): print('*' * 5, 'DATA NOTIF:', data._getstatusname(status), *args) if status == data.LIVE: self.counttostop = self.p.stopafter self.datastatus = 1 def notify_store(self, msg, *args, **kwargs): print('*' * 5, 'STORE NOTIF:', msg) def notify_order(self, order): if order.status in [order.Completed, order.Cancelled, order.Rejected]: self.order = None print('-' * 50, 'ORDER BEGIN', datetime.datetime.now()) print(order) print('-' * 50, 'ORDER END') def notify_trade(self, trade): print('-' * 50, 'TRADE BEGIN', datetime.datetime.now()) print(trade) print('-' * 50, 'TRADE END') def prenext(self): self.next(frompre=True) def next(self, frompre=False): txt = list() txt.append('%04d' % len(self)) dtfmt = '%Y-%m-%dT%H:%M:%S.%f' txt.append('%s' % self.data.datetime.datetime(0).strftime(dtfmt)) txt.append('{}'.format(self.data.open[0])) txt.append('{}'.format(self.data.high[0])) txt.append('{}'.format(self.data.low[0])) txt.append('{}'.format(self.data.close[0])) txt.append('{}'.format(self.data.volume[0])) txt.append('{}'.format(self.data.openinterest[0])) txt.append('{}'.format(self.sma[0])) print(', '.join(txt)) if len(self.datas) > 1: txt = list() txt.append('%04d' % len(self)) dtfmt = '%Y-%m-%dT%H:%M:%S.%f' txt.append('%s' % self.data1.datetime.datetime(0).strftime(dtfmt)) txt.append('{}'.format(self.data1.open[0])) txt.append('{}'.format(self.data1.high[0])) txt.append('{}'.format(self.data1.low[0])) txt.append('{}'.format(self.data1.close[0])) txt.append('{}'.format(self.data1.volume[0])) txt.append('{}'.format(self.data1.openinterest[0])) txt.append('{}'.format(float('NaN'))) print(', '.join(txt)) if self.counttostop: # stop after x live lines self.counttostop -= 1 if not self.counttostop: self.env.runstop() return if not self.p.trade: return if self.datastatus and not self.position and len(self.orderid) < 1: self.order = self.buy(size=self.p.stake, exectype=self.p.exectype, price=round(self.data0.close[0] * 0.90, 2), valid=self.p.valid) self.orderid.append(self.order) elif self.position.size > 0 and not self.p.donotsell: if self.order is None: self.order = self.sell(size=self.p.stake // 2, exectype=bt.Order.Market, price=self.data0.close[0]) elif self.order is not None and self.p.cancel: if self.datastatus > self.p.cancel: self.cancel(self.order) if self.datastatus: self.datastatus += 1 def start(self): if self.data0.contractdetails is not None: print('Timezone from ContractDetails: {}'.format( self.data0.contractdetails.m_timeZoneId)) header = ['Datetime', 'Open', 'High', 'Low', 'Close', 'Volume', 'OpenInterest', 'SMA'] print(', '.join(header)) self.done = False def runstrategy(): args = parse_args() # Create a cerebro cerebro = bt.Cerebro() storekwargs = dict( host=args.host, port=args.port, clientId=args.clientId, timeoffset=not args.no_timeoffset, reconnect=args.reconnect, timeout=args.timeout, notifyall=args.notifyall, _debug=args.debug ) if args.usestore: ibstore = bt.stores.IBStore(**storekwargs) if args.broker: if args.usestore: broker = ibstore.getbroker() else: broker = bt.brokers.IBBroker(**storekwargs) cerebro.setbroker(broker) timeframe = bt.TimeFrame.TFrame(args.timeframe) if args.resample or args.replay: datatf = bt.TimeFrame.Ticks datacomp = 1 else: datatf = timeframe datacomp = args.compression fromdate = None if args.fromdate: dtformat = '%Y-%m-%d' + ('T%H:%M:%S' * ('T' in args.fromdate)) fromdate = datetime.datetime.strptime(args.fromdate, dtformat) IBDataFactory = ibstore.getdata if args.usestore else bt.feeds.IBData datakwargs = dict( timeframe=datatf, compression=datacomp, historical=args.historical, fromdate=fromdate, rtbar=args.rtbar, qcheck=args.qcheck, what=args.what, backfill_start=not args.no_backfill_start, backfill=not args.no_backfill, latethrough=args.latethrough, tz=args.timezone ) if not args.usestore and not args.broker: # neither store nor broker datakwargs.update(storekwargs) # pass the store args over the data data0 = IBDataFactory(dataname=args.data0, **datakwargs) data1 = None if args.data1 is not None: data1 = IBDataFactory(dataname=args.data1, **datakwargs) rekwargs = dict( timeframe=timeframe, compression=args.compression, bar2edge=not args.no_bar2edge, adjbartime=not args.no_adjbartime, rightedge=not args.no_rightedge, takelate=not args.no_takelate, ) if args.replay: cerebro.replaydata(dataname=data0, **rekwargs) if data1 is not None: cerebro.replaydata(dataname=data1, **rekwargs) elif args.resample: cerebro.resampledata(dataname=data0, **rekwargs) if data1 is not None: cerebro.resampledata(dataname=data1, **rekwargs) else: cerebro.adddata(data0) if data1 is not None: cerebro.adddata(data1) if args.valid is None: valid = None else: datetime.timedelta(seconds=args.valid) # Add the strategy cerebro.addstrategy(TestStrategy, smaperiod=args.smaperiod, trade=args.trade, exectype=bt.Order.ExecType(args.exectype), stake=args.stake, stopafter=args.stopafter, valid=valid, cancel=args.cancel, donotsell=args.donotsell) # Live data ... avoid long data accumulation by switching to "exactbars" cerebro.run(exactbars=args.exactbars) if args.plot and args.exactbars < 1: # plot if possible cerebro.plot() def parse_args(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Test Interactive Brokers integration') parser.add_argument('--exactbars', default=1, type=int, required=False, action='store', help='exactbars level, use 0/-1/-2 to enable plotting') parser.add_argument('--plot', required=False, action='store_true', help='Plot if possible') parser.add_argument('--stopafter', default=0, type=int, required=False, action='store', help='Stop after x lines of LIVE data') parser.add_argument('--usestore', required=False, action='store_true', help='Use the store pattern') parser.add_argument('--notifyall', required=False, action='store_true', help='Notify all messages to strategy as store notifs') parser.add_argument('--debug', required=False, action='store_true', help='Display all info received form IB') parser.add_argument('--host', default='127.0.0.1', required=False, action='store', help='Host for the Interactive Brokers TWS Connection') parser.add_argument('--qcheck', default=0.5, type=float, required=False, action='store', help=('Timeout for periodic ' 'notification/resampling/replaying check')) parser.add_argument('--port', default=7496, type=int, required=False, action='store', help='Port for the Interactive Brokers TWS Connection') parser.add_argument('--clientId', default=None, type=int, required=False, action='store', help='Client Id to connect to TWS (default: random)') parser.add_argument('--no-timeoffset', required=False, action='store_true', help=('Do not Use TWS/System time offset for non ' 'timestamped prices and to align resampling')) parser.add_argument('--reconnect', default=3, type=int, required=False, action='store', help='Number of recconnection attempts to TWS') parser.add_argument('--timeout', default=3.0, type=float, required=False, action='store', help='Timeout between reconnection attempts to TWS') parser.add_argument('--data0', default=None, required=True, action='store', help='data 0 into the system') parser.add_argument('--data1', default=None, required=False, action='store', help='data 1 into the system') parser.add_argument('--timezone', default=None, required=False, action='store', help='timezone to get time output into (pytz names)') parser.add_argument('--what', default=None, required=False, action='store', help='specific price type for historical requests') parser.add_argument('--no-backfill_start', required=False, action='store_true', help='Disable backfilling at the start') parser.add_argument('--latethrough', required=False, action='store_true', help=('if resampling replaying, adjusting time ' 'and disabling time offset, let late samples ' 'through')) parser.add_argument('--no-backfill', required=False, action='store_true', help='Disable backfilling after a disconnection') parser.add_argument('--rtbar', default=False, required=False, action='store_true', help='Use 5 seconds real time bar updates if possible') parser.add_argument('--historical', required=False, action='store_true', help='do only historical download') parser.add_argument('--fromdate', required=False, action='store', help=('Starting date for historical download ' 'with format: YYYY-MM-DD[THH:MM:SS]')) parser.add_argument('--smaperiod', default=5, type=int, required=False, action='store', help='Period to apply to the Simple Moving Average') pgroup = parser.add_mutually_exclusive_group(required=False) pgroup.add_argument('--replay', required=False, action='store_true', help='replay to chosen timeframe') pgroup.add_argument('--resample', required=False, action='store_true', help='resample to chosen timeframe') parser.add_argument('--timeframe', default=bt.TimeFrame.Names[0], choices=bt.TimeFrame.Names, required=False, action='store', help='TimeFrame for Resample/Replay') parser.add_argument('--compression', default=1, type=int, required=False, action='store', help='Compression for Resample/Replay') parser.add_argument('--no-takelate', required=False, action='store_true', help=('resample/replay, do not accept late samples ' 'in new bar if the data source let them through ' '(latethrough)')) parser.add_argument('--no-bar2edge', required=False, action='store_true', help='no bar2edge for resample/replay') parser.add_argument('--no-adjbartime', required=False, action='store_true', help='no adjbartime for resample/replay') parser.add_argument('--no-rightedge', required=False, action='store_true', help='no rightedge for resample/replay') parser.add_argument('--broker', required=False, action='store_true', help='Use IB as broker') parser.add_argument('--trade', required=False, action='store_true', help='Do Sample Buy/Sell operations') parser.add_argument('--donotsell', required=False, action='store_true', help='Do not sell after a buy') parser.add_argument('--exectype', default=bt.Order.ExecTypes[0], choices=bt.Order.ExecTypes, required=False, action='store', help='Execution to Use when opening position') parser.add_argument('--stake', default=10, type=int, required=False, action='store', help='Stake to use in buy operations') parser.add_argument('--valid', default=None, type=int, required=False, action='store', help='Seconds to keep the order alive (0 means DAY)') parser.add_argument('--cancel', default=0, type=int, required=False, action='store', help=('Cancel a buy order after n bars in operation,' ' to be combined with orders like Limit')) return parser.parse_args() if __name__ == '__main__': runstrategy()
节省内存
原文:
www.backtrader.com/blog/posts/2016-05-09-memory-savings/memory-savings/
版本 1.3.1.92 已经重新设计并完全实现了之前的内存节省方案,虽然之前并没有被过多宣传和使用。
发布:github.com/mementum/backtrader/releases/tag/1.3.1.92
backtrader
在拥有很好内存量的机器上(并将进一步开发)开发,加上通过绘图进行可视化反馈几乎是必需的,这使得设计决策变得容易:将所有内容保存在内存中。
这个决定有一些缺点:
array.array
用于数据存储,当超过一些边界时必须分配和移动数据- 具有较少内存的机器可能会受到影响。
- 连接到在线数据源,可以在线运行数周/数月,并将数千秒/分钟的分辨率 tick 送入系统
后者比第一个更重要,因为为backtrader
做出了另一个设计决策:
- 需要是纯 Python,以便在需要时在嵌入式系统中运行。
将来的一个场景可能是backtrader
连接到提供实时数据源的第二台机器,而backtrader
本身运行在一个树莓派或者甚至更有限的设备上,比如一个 ADSL 路由器(AVM Frit!Box 7490,配备Freetz镜像)。
因此需要backtrader
支持动态内存方案。现在可以使用以下语义来实例化或运行Cerebro
:
- exactbars(默认值:False)
默认为False
值时,每个线条中存储的每个值都会保留在内存中。
可能的值:
- `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** - `-1`: datas and indicators/operations at strategy level will keep all data in memory. For example: a `RSI` internally uses the indicator `UpDay` to make calculations. This subindicator will not keep all data in memory - This allows to keep `plotting` and `preloading` active. - `runonce` will be deactivated - `-2`: datas and indicators kept as attributes of the strategy will keep all data in memory. For example: a `RSI` internally uses the indicator `UpDay` to make calculations. This subindicator will not keep all data in memory If in the `__init__` something like `a = self.data.close - self.data.high` is defined, then `a` will not keep all data in memory - This allows to keep `plotting` and `preloading` active. - `runonce` will be deactivated`
一如既往,例子胜过千言万语。一个样本脚本显示了差异。它针对 1996 年至 2015 年的Yahoo每日数据运行,总计4965
天。
注意
这只是一个小样本。每天交易 14 小时的 EuroStoxx50 期货品种,在只有 1 个月的交易中将产生约 18000 个 1 分钟 K 线。
执行第一个脚本以查看在不请求内存节省时使用了多少内存位置:
$ ./memory-savings.py --save 0 Total memory cells used: 506430
对于级别 1(总节省):
$ ./memory-savings.py --save 1 Total memory cells used: 2041
天啊!!!从一百万降至2041
。确实。系统中的每个lines对象都使用collections.deque
作为缓冲区(而不是array.array
)并且被长度边界到请求操作的绝对最小值。例如:
- 在数据源上使用周期为
30
的SimpleMovingAverage
的策略。
在这种情况下,将进行以下调整:
- 数据源 将有一个
30
位置的缓冲区,这是由SimpleMovingAverage
产生下一个值所需的量。 SimpleMovingAverage
将有一个1
位置的缓冲区,因为除非被其他指标(依赖移动平均线)需要,否则没有必要保留更大的缓冲区。
注意
这种模式最吸引人且可能最重要的特点是脚本的整个生命周期内使用的内存量保持恒定。
无论数据源的大小如何。
如果长时间连接到实时数据源,这将非常有用。
但要考虑:
- 绘图不可用
- 还有其他会随时间累积的内存消耗源,比如策略生成的
orders
。 - 这种模式只能在
cerebro
中使用runonce=False
。这对于实时数据源是强制的,但在简单的回测中,这比runonce=True
慢。
肯定存在一个权衡点,即内存管理比逐步执行回测更昂贵,但这只能由平台的最终用户根据具体情况来判断。
现在是负级别。这些级别旨在在保存足够的内存的同时保持绘图可用。第一级别为-1
:
$ ./memory-savings.py --save -1 Total memory cells used: 184623
在这种情况下,指标的第一级(在策略中声明的那些)保持其完整长度的缓冲区。但如果这些指标依赖于其他指标(这是情况),以执行其工作,那么子对象将被限制长度。在这种情况下,我们从:
506430
个内存位置变为184623
个
超过 50%的节省。
注意
当然,array.array
对象已被更换为在内存方面更昂贵但在操作方面更快的collections.deque
。但collection.deque
对象相当小,节省的内存位置大致相等。
现在是第二级,也旨在节省在策略级别声明为不绘制的指标上的内存:
$ ./memory-savings.py --save -2 Total memory cells used: 174695
现在并没有节省太多。这是因为一个单独的指标被标记为不绘制:TestInd().plotinfo.plot = False
让我们看看最后一个示例的绘图:
$ ./memory-savings.py --save -2 --plot Total memory cells used: 174695
对于感兴趣的读者,示例脚本可以生成对指标层次结构中遍历的每个lines对象的详细分析。运行时启用绘图(保存在-1
处):
$ ./memory-savings.py --save -1 --lendetails -- Evaluating Datas ---- Data 0 Total Cells 34755 - Cells per Line 4965 -- Evaluating Indicators ---- Indicator 1.0 Average Total Cells 30 - Cells per line 30 ---- SubIndicators Total Cells 1 ---- Indicator 1.1 _LineDelay Total Cells 1 - Cells per line 1 ---- SubIndicators Total Cells 1 ... ---- Indicator 0.5 TestInd Total Cells 9930 - Cells per line 4965 ---- SubIndicators Total Cells 0 -- Evaluating Observers ---- Observer 0 Total Cells 9930 - Cells per Line 4965 ---- Observer 1 Total Cells 9930 - Cells per Line 4965 ---- Observer 2 Total Cells 9930 - Cells per Line 4965 Total memory cells used: 184623
启用最大节省(1
)的相同代码:
$ ./memory-savings.py --save 1 --lendetails -- Evaluating Datas ---- Data 0 Total Cells 266 - Cells per Line 38 -- Evaluating Indicators ---- Indicator 1.0 Average Total Cells 30 - Cells per line 30 ---- SubIndicators Total Cells 1 ... ---- Indicator 0.5 TestInd Total Cells 2 - Cells per line 1 ---- SubIndicators Total Cells 0 -- Evaluating Observers ---- Observer 0 Total Cells 2 - Cells per Line 1 ---- Observer 1 Total Cells 2 - Cells per Line 1 ---- Observer 2 Total Cells 2 - Cells per Line 1
第二个输出立即显示了数据源中的行数被限制为38
个内存位置,而不是完整数据源长度的4965
个。
并且指标和观察者在可能的情况下被限制为1
,如输出的最后几行所示。
BackTrader 中文文档(二十四)(3)https://developer.aliyun.com/article/1505449