社区用户@randyt
已经能够将反向交易者扩展到其极限。找到一些晦涩难懂的角落,甚至在这里和那里添加pdb
语句,这一直是获得更精细的重采样流同步的驱动力。
最近, @randyt
添加了一个拉取请求以集成一个名为PercentRank
的新指针。这是原始代码
class PercentRank(bt.Indicator): lines = ('pctrank',) params = (('period', 50),) def __init__(self): self.addminperiod(self.p.period) def next(self): self.lines.pctrank[0] = \ (math.fsum([x < self.data[0] for x in self.data.get(size=self.p.period)]) / self.p.period) super(PercentRank, self).__init__()
它确实显示了有人如何进入backtrader的源代码,提出一些问题并掌握一些概念。这真的很好:
self.addminperiod(self.p.period)
出乎意料,因为最终用户甚至不会知道有人可以在行对像中使用该 API 调用。此调用告诉机器确保指针至少有可用的数据馈送的self.p.period
样本,因为它们是计算所必需的。
在原始代码中可以看到self.data.get(size=self.p.period)
,只有在后台引擎确保在进行第一次计算之前有这么多样本可用时(并且如果exactbars
用于减少内存使用,那些许多样本总是在那里)
初始重新加载
可以重新编写代码以利用旨在缓解开发的预先存在的实用进程。最终用户无需注意任何事情,但很高兴知道一个人是否在不断地开发或原型设计指针。
class PercentRank_PeriodN1(bt.ind.PeriodN): lines = ('pctrank',) params = (('period', 50),) def next(self): d0 = self.data[0] # avoid dict/array lookups each time dx = self.data.get(size=self.p.period) self.l.pctrank[0] = math.fsum((x < d0 for x in dx)) / self.p.period
重用PeriodN
是消除self.addminperiod
魔法并使指针在某种程度上更适合的关键。 PeriodN
已经具有一个period
参数,并将为用户进行调用(如果__init__
被覆盖,请记住调用super(cls, self).__init__()
。
计算分为 3行,首先缓存字典和数组查找并使其更具可读性(尽管后者只是口味问题)
代码也从 13 行减少到 8行。这通常在阅读时有所帮助。
通过 OperationN 重新加载
现有的指针,如SumN
,它对一段时间内数据源的值求和,不像上面那样直接创建在PeriodN
上,而是创建在它的一个名为OperationN
的子类上。像它的父类一样,它仍然没有定义任何行,并且有一个名为func
的类属性。
func
将使用一个数组调用,该数组包含主机函数必须操作的时间段的数据。签名基本上是: func(data[0:period])
并返回适合存储在line中的内容,即:浮点值。
知道了,我们可以试试显而易见的
class PercentRank_OperationN1(bt.ind.OperationN): lines = ('pctrank',) params = (('period', 50),) func = (lambda d: math.fsum((x < d[-1] for x in d)) / self.p.period)
降到 4行。但这将失败(只需要最后一行):
TypeError: <lambda>() takes 1 positional argument but 2 were given
(使用--strat n1= True
使样本失败)
通过将我们未命名的函数放在func
中,它似乎已经变成了一个方法,因为它需要两个参数。这可以很快治愈。
class PercentRank_OperationN2(bt.ind.OperationN): lines = ('pctrank',) params = (('period', 50),) func = (lambda self, d: math.fsum((x < d[-1] for x in d)) / self.p.period)
它有效。但是有一些丑陋的东西:这不是人们大多数时候期望的传递函数的方式,即:将self
作为参数。在这种情况下,我们可以控制函数,但情况可能并非总是如此(需要一个包装器来解决它)
Python 中的语法糖用staticmethod
来拯救,但在我们这样做之前,我们知道在staticmethod
中不再可能引用self.p.period
,失去像以前一样进行平均计算的能力。
但是由于func
接收一个固定长度的可迭代对象,所以可以使用len
。
现在是新代码。
class PercentRank_OperationN3(bt.ind.OperationN): lines = ('pctrank',) params = (('period', 50),) func = staticmethod(lambda d: math.fsum((x < d[-1] for x in d)) / len(d))
一切都很好,但这让人们思考了为什么以前没有考虑过让用户有机会传递他们自己的功能。子类化OperationN
是一个不错的选择,但可以避免使用staticmethod
或将self
作为参数并在backtrader的机制上构建更好的方法。
让我们定义一个方便的OperationN
子类。
class ApplyN(bt.ind.OperationN): lines = ('apply',) params = (('func', None),) def __init__(self): self.func = self.p.func super(ApplyN, self).__init__()
这可能早就应该在平台上。这里唯一真正的辨别是是否必须有lines = ('apply',)
或者用户应该可以自由定义该行和其他一些行。在集成之前需要考虑的事情。
有了ApplyN
, PercentRank
的最终版本完全符合我们的预期。一是手动平均计算的版本。
class PercentRank_ApplyN(ApplyN): params = ( ('period', 50), ('func', lambda d: math.fsum((x < d[-1] for x in d)) / len(d)), )
在不破坏PEP-8
情况下,我们仍然可以重新格式化两者以适应 3行……好!
让我们运行示例
下面可以看到的示例具有通常的骨架样板,但旨在显示不同PercentRank
实现的视觉比较。
笔记
使用--strat n1= True
运行它以尝试不起作用的PercentRank_OperationN1
版本
图形输出。
示例使用
$ ./percentrank.py --help usage: percentrank.py [-h] [--data0 DATA0] [--fromdate FROMDATE] [--todate TODATE] [--cerebro kwargs] [--broker kwargs] [--sizer kwargs] [--strat kwargs] [--plot [kwargs]] Sample Skeleton optional arguments: -h, --help show this help message and exit --data0 DATA0 Data to read in (default: ../../datas/2005-2006-day-001.txt) --fromdate FROMDATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --todate TODATE Date[time] in YYYY-MM-DD[THH:MM:SS] format (default: ) --cerebro kwargs kwargs in key=value format (default: ) --broker kwargs kwargs in key=value format (default: ) --sizer kwargs kwargs in key=value format (default: ) --strat kwargs kwargs in key=value format (default: ) --plot [kwargs] kwargs in key=value format (default: )
示例代码
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import math import backtrader as bt class PercentRank(bt.Indicator): lines = ('pctrank',) params = (('period', 50),) def __init__(self): self.addminperiod(self.p.period) def next(self): self.lines.pctrank[0] = \ (math.fsum([x < self.data[0] for x in self.data.get(size=self.p.period)]) / self.p.period) super(PercentRank, self).__init__() class PercentRank_PeriodN1(bt.ind.PeriodN): lines = ('pctrank',) params = (('period', 50),) def next(self): d0 = self.data[0] # avoid dict/array lookups each time dx = self.data.get(size=self.p.period) self.l.pctrank[0] = math.fsum((x < d0 for x in dx)) / self.p.period class PercentRank_OperationN1(bt.ind.OperationN): lines = ('pctrank',) params = (('period', 50),) func = (lambda d: math.fsum((x < d[-1] for x in d)) / self.p.period) class PercentRank_OperationN2(bt.ind.OperationN): lines = ('pctrank',) params = (('period', 50),) func = (lambda self, d: math.fsum((x < d[-1] for x in d)) / self.p.period) class PercentRank_OperationN3(bt.ind.OperationN): lines = ('pctrank',) params = (('period', 50),) func = staticmethod(lambda d: math.fsum((x < d[-1] for x in d)) / len(d)) class ApplyN(bt.ind.OperationN): lines = ('apply',) params = (('func', None),) def __init__(self): self.func = self.p.func super(ApplyN, self).__init__() class PercentRank_ApplyN(ApplyN): params = ( ('period', 50), ('func', lambda d: math.fsum((x < d[-1] for x in d)) / len(d)), ) class St(bt.Strategy): params = ( ('n1', False), ) def __init__(self): PercentRank() PercentRank_PeriodN1() if self.p.n1: PercentRank_OperationN1() PercentRank_OperationN2() PercentRank_OperationN3() PercentRank_ApplyN() def next(self): pass def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # Data feed kwargs kwargs = dict() # Parse from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): if a: strpfmt = dtfmt + tmfmt * ('T' in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) # Data feed data0 = bt.feeds.BacktraderCSVData(dataname=args.data0, **kwargs) cerebro.adddata(data0) # Broker cerebro.broker = bt.brokers.BackBroker(**eval('dict(' + args.broker + ')')) # Sizer cerebro.addsizer(bt.sizers.FixedSize, **eval('dict(' + args.sizer + ')')) # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) # Execute cerebro.run(**eval('dict(' + args.cerebro + ')')) if args.plot: # Plot if requested to cerebro.plot(**eval('dict(' + args.plot + ')')) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=( 'Sample Skeleton' ) ) parser.add_argument('--data0', default='../../datas/2005-2006-day-001.txt', required=False, help='Data to read in') # Defaults for dates parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') parser.add_argument('--todate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') parser.add_argument('--cerebro', required=False, default='', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--broker', required=False, default='', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--sizer', required=False, default='', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--strat', required=False, default='', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--plot', required=False, default='', nargs='?', const='{}', metavar='kwargs', help='kwargs in key=value format') return parser.parse_args(pargs) if __name__ == '__main__': runstrat()