社區用戶@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()