BackTrader 中文文档(十八)(3)https://developer.aliyun.com/article/1505399
百分位重加载
原文:
www.backtrader.com/blog/posts/2017-02-05-percentrank-reloaded/percentrank-reloaded/
社区用户@randyt
已经能够将backtrader推至极限。找到一些晦涩的角落,甚至在那里添加了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)
出乎意料,因为最终用户甚至不会预期到某人可以在lines对象中使用该 API 调用。这个调用告诉机器确保指标至少有self.p.period
个data feeds样本可用,因为它们需要用于计算。
在原始代码中可以看到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
的子类上。与其父类一样,它仍然不定义lines,并且具有一个名为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()
数字交叉
原文:
www.backtrader.com/blog/posts/2017-02-04-crossing-over-numbers/crossing-over-numbers/
在backtrader的1.9.27.105
版本中已经纠正了一个疏忽。这是一个疏忽,因为所有的拼图都已经就位,但并没有在所有角落进行激活。
该机制使用一个名为_mindatas
的属性,让我们称之为:mindatas
。
社区提出了问题,答案并不完全正确。请查看这里的对话:
即使对话是关于其他事情的,问题也可以很快得到回答:“嘿,实际上应该可以工作!”但是现在谁有时间考虑一个恰当而周到的答案呢。
让我们考虑穿越一个普通的数字参数的用例。类似这样
mycrossover = bt.ind.CrossOver(bt.ind.RSI(), 50.0)
这将会像这样断开
Traceback (most recent call last): File "./cross-over-num.py", line 114, in <module> runstrat() File "./cross-over-num.py", line 70, in runstrat cerebro.run(**eval('dict(' + args.cerebro + ')')) File "d:\dro\01-docs\01-home\src\backtrader\backtrader\cerebro.py", line 810, in run runstrat = self.runstrategies(iterstrat) File "d:\dro\01-docs\01-home\src\backtrader\backtrader\cerebro.py", line 878, in runstrategies strat = stratcls(*sargs, **skwargs) File "d:\dro\01-docs\01-home\src\backtrader\backtrader\metabase.py", line 87, in __call__ _obj, args, kwargs = cls.doinit(_obj, *args, **kwargs) File "d:\dro\01-docs\01-home\src\backtrader\backtrader\metabase.py", line 77, in doinit _obj.__init__(*args, **kwargs) File "./cross-over-num.py", line 35, in __init__ bt.ind.CrossOver(bt.ind.RSI(), 50) File "d:\dro\01-docs\01-home\src\backtrader\backtrader\indicator.py", line 53, in __call__ return super(MetaIndicator, cls).__call__(*args, **kwargs) File "d:\dro\01-docs\01-home\src\backtrader\backtrader\metabase.py", line 87, in __call__ _obj, args, kwargs = cls.doinit(_obj, *args, **kwargs) File "d:\dro\01-docs\01-home\src\backtrader\backtrader\metabase.py", line 77, in doinit _obj.__init__(*args, **kwargs) Typeerror: __init__() takes exactly 1 argument (2 given)
最后一行最具信息性,因为它告诉我们有太多的参数。这意味着50.0
正在伤害我们。
为了解决手头的问题,给出了一个数字包装器作为答案。
class ConstantValue(bt.Indicator): lines = ('constant',) params = (('constant', float('NaN')),) def next(self): self.lines.constant[0] = self.p.constant ... mycrossover = bt.ind.CrossOver(bt.ind.RSI(), ConstantValue(50.0))
问题解决了。但等等,解决方案已经在手边。有一个内部助手,用于解决问题,但被完全遗忘了:LineNum
。它做的就是名字所暗示的:获取一个数字并将其变成一行。问题的解决方案就在那里,解决方案可能看起来像这样:
mycrossover = bt.ind.CrossOver(bt.ind.RSI(), bt.LineNum(50.0))
通常的后台线程仍在不断地运行,告诉我们仍然有一些地方不是 100%清晰,解决方案应该是显而易见的,而不需要用户指定包装器。
然后出现了疏忽。即使mindatas
机制存在并应用于系统的某些部分,但并没有应用于CrossOver
。尝试过,但有时人类会失败,他们相信自己已经做了某事,结果发现他们没有往下滚动。这就是情况。像这样添加一行代码:
class CrossOver(Indicator): ... _mindatas = 2 ...
现在问题的解决方案很明显:
mycrossover = bt.ind.CrossOver(bt.ind.RSI(), 50.0)
应该一直是这样的方式(参见下面的示例和图表)
mindatas
在工作
这是一个方便的属性,旨在用于特定情况,因此前面有_
,表示应该非常谨慎使用。指标的默认值是:
_mindatas = 1
这告诉系统,如果没有向指标传递任何数据源,系统应该从父级复制第一个数据源。如果没有这个,例如实例化RelativeStrengthIndicator
应该这样做:
class Strategy(bt.Indicator): def __init__(self): rsi = bt.ind.RSI(self.data0)`
- 但是使用
_mindatas
给出的默认指示,以下是可能的:
class Strategy(bt.Indicator): def __init__(self): rsi = bt.ind.RSI()`
- 结果完全相同,因为策略中的第一个数据源
self.data0
被传递给RSI
的实例化
像 CrossOver
这样的指示器需要 2 个数据源,因为它正在检查一件事是否穿过另一件事。在这种情况下,并如上所示,默认值已设置为:
_mindatas = 2
这告诉系统一些信息,比如:
- 如果没有数据被传递,则从父级复制 2 个数据源(如果可能的话)。
- 如果只传递了 1 个数据,尝试将下一个传入的参数转换为 lines 对象,以便有 2 个数据源可用。对于普通浮点数的线穿越用例很有用。再次参考:
mycrossover = bt.ind.CrossOver(bt.ind.RSI(), 50.0)`
- 如果向
CrossOver
传递了 2 个或更多个数据源,则不执行任何操作,并继续执行。
在社区中,最近已经将该机制应用于例如实现配对交易的 KalmanFilter
的第一稿。当谈论到配对时,需要 2 个数据源,因此:_mindatas = 2
一个小示例(尽管有一个完整的框架)来测试完整的解决方案:
$ ./cross-over-num.py --plot
这将产生这样的输出。
示例用法
$ ./cross-over-num.py --help usage: cross-over-num.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 backtrader as bt class St(bt.Strategy): params = () def __init__(self): bt.ind.CrossOver(bt.ind.RSI(), 50) 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()