使用次數越多, backtrader 必須面對的想法和意外場景的混合就越多。對於每個新平臺,一個挑戰是要看看平臺是否能夠達到開發開始時設定的期望,靈活性和易用性是目標,Python被選為基石。
工單#76 提出了一個問題,即是否可以完成具有不同交易日曆的同步市場。直接嘗試這樣做失敗了,問題建立者想知道為什麼不 backtrader
查看日期。
在給出任何答案之前,必須考慮以下幾點:
- 未對齊的天數的指示器的行為
對後者的回答是:
- 該平台盡可能地
date
和time
不可知論,不會查看字段的內容來評估這些概念
考慮到股票市場價格是datetime
序列的事實,上述可以保持 true 達到一定的限度。對於多個數據,以下設計注意事項適用:
-
添加到
cerebro
的第 1個數據是datamaster
-
所有其他數據必須在時間上對齊/同步,它永遠無法超越(就
datetime
而言)datamaster
將上面的3個項目符號放在一起,可以提供問題創建者所經歷的組合。場景:
-
歷年:
2012
-
數據0:
^GSPC
(或朋友的標準普爾500指數) -
資料1:
^GDAXI
(或朋友的Dax指數)
透過以下方式執行自訂文稿以檢視資料是如何同步的backtrader
:
$ ./weekdaysaligner.py --online --data1 '^GSPC' --data0 '^GDAXI'
輸出:
0001, True, data0, 2012-01-03T23:59:59, 2012-01-03T23:59:59, data1 0002, True, data0, 2012-01-04T23:59:59, 2012-01-04T23:59:59, data1 0003, True, data0, 2012-01-05T23:59:59, 2012-01-05T23:59:59, data1 0004, True, data0, 2012-01-06T23:59:59, 2012-01-06T23:59:59, data1 0005, True, data0, 2012-01-09T23:59:59, 2012-01-09T23:59:59, data1 0006, True, data0, 2012-01-10T23:59:59, 2012-01-10T23:59:59, data1 0007, True, data0, 2012-01-11T23:59:59, 2012-01-11T23:59:59, data1 0008, True, data0, 2012-01-12T23:59:59, 2012-01-12T23:59:59, data1 0009, True, data0, 2012-01-13T23:59:59, 2012-01-13T23:59:59, data1 0010, False, data0, 2012-01-17T23:59:59, 2012-01-16T23:59:59, data1 0011, False, data0, 2012-01-18T23:59:59, 2012-01-17T23:59:59, data1 ...
一旦2012-01-16
交易日曆發散。這是 data0
datamaster
(^GSPC
),即使 data1
(^GDAXI
)會有一個柱線來兌現 2012-01-16
,這不是標準普爾500指數 的交易日 。
最好的是,backtrader
當下一個交易日 ^GSPC
進來時,可以對上述設計限制做些什麼, 2012-01-17
交付下一個尚未處理的日期 ^GDAXI
是 2012-01-16
.
同步問題隨著每個發散日而累積。在它的末尾2012
如下所示:
... 0249, False, data0, 2012-12-28T23:59:59, 2012-12-19T23:59:59, data1 0250, False, data0, 2012-12-31T23:59:59, 2012-12-20T23:59:59, data1
原因應該很明顯:歐洲人比美國人交易的天數更多。
在票證 #76https://github.com/mementum/backtrader/issues/76 中,海報顯示了什麼 zipline
。讓我們來看看 2012-01-13
- 2012-01-17
難題:
0009 : True : 2012-01-13 : close 1289.09 - 2012-01-13 : close 6143.08 0010 : False : 2012-01-13 : close 1289.09 - 2012-01-16 : close 6220.01 0011 : True : 2012-01-17 : close 1293.67 - 2012-01-17 : close 6332.93
起泡的藤壺!2012-01-13
的數據只是簡單地 複製, 而沒有明顯要求用戶許可。恕我直言,這不應該是因為平臺的最終用戶無法撤消此自發添加。
注意
除了簡要介紹zipline
之外,作者不知道這是否是腳本開發人員配置的標準行為,以及是否可以撤消
一旦我們看到其他人,讓我們再次backtrader
嘗試使用積累的智慧:歐洲人比美國人更頻繁地交易。讓我們反轉 和 ^GDAXI
的角色^GSPC
,看看結果:
$ ./weekdaysaligner.py --online --data1 '^GSPC' --data0 '^GDAXI'
輸出(跳到2012-01-13
直接):
... 0009, True, data0, 2012-01-13T23:59:59, 2012-01-13T23:59:59, data1 0010, False, data0, 2012-01-16T23:59:59, 2012-01-13T23:59:59, data1 0011, True, data0, 2012-01-17T23:59:59, 2012-01-17T23:59:59, data1 ...
再次起泡藤壺!backtrader
還複製了 2012-01-13
(在本例^GSPC
中 ) 的值data1
作為 (現在 ^GDAXI
) 交付的2012-01-16
匹配data0
項。
甚至更好:
- 在下一個日期實現同步:
2012-01-17
同樣的重新同步很快就會再次出現:
... 0034, True, data0, 2012-02-17T23:59:59, 2012-02-17T23:59:59, data1 0035, False, data0, 2012-02-20T23:59:59, 2012-02-17T23:59:59, data1 0036, True, data0, 2012-02-21T23:59:59, 2012-02-21T23:59:59, data1 ...
其次是不太容易的重新同步:
... 0068, True, data0, 2012-04-05T23:59:59, 2012-04-05T23:59:59, data1 0069, False, data0, 2012-04-10T23:59:59, 2012-04-09T23:59:59, data1 ... 0129, False, data0, 2012-07-04T23:59:59, 2012-07-03T23:59:59, data1 0130, True, data0, 2012-07-05T23:59:59, 2012-07-05T23:59:59, data1 ...
這樣的情節不斷重複,直到 last 欄^GDAXI
交付:
... 0256, True, data0, 2012-12-31T23:59:59, 2012-12-31T23:59:59, data1 ...
此同步問題backtrader
的原因是不會複製數據。
-
datamaster
一旦交付了一個新的酒吧,另datas
一個就會被要求交付 -
如果沒有柱線可以傳遞電流
datetime
datamaster
(例如,因為它將被超越),那麼下一個最佳數據就是,可以說,重新傳遞這是一個已經看到的酒吧
date
正確同步
但並非所有的希望都消失了。backtrader
可以交付。讓我們使用 篩檢程式。 backtrader
這項技術允許在數據到達平臺最深處之前對其進行操作,例如計算指標。
注意
delivering
是一個感知問題,因此交付的 backtrader
可能不是接收者所期望的 delivery
實際代碼
from __future__ import (absolute_import, division, print_function, unicode_literals) import datetime class WeekDaysFiller(object): '''Bar Filler to add missing calendar days to trading days''' # kickstart value for date comparisons lastdt = datetime.datetime.max.toordinal() def __init__(self, data, fillclose=False): self.fillclose = fillclose self.voidbar = [float('Nan')] * data.size() # init a void bar def __call__(self, data): '''Empty bars (NaN) or with last close price are added for weekdays with no data Params: - data: the data source to filter/process Returns: - True (always): bars are removed (even if put back on the stack) ''' dt = data.datetime.dt() # current date in int format lastdt = self.lastdt + 1 # move the last seen data once forward while lastdt < dt: # loop over gap bars if datetime.date.fromordinal(lastdt).isoweekday() < 6: # Mon-Fri # Fill in date and add new bar to the stack if self.fillclose: self.voidbar = [self.lastclose] * data.size() self.voidbar[-1] = float(lastdt) + data.sessionend data._add2stack(self.voidbar[:]) lastdt += 1 # move lastdt forward self.lastdt = dt # keep a record of the last seen date self.lastclose = data.close[0] data._save2stack(erase=True) # dt bar to the stack and out of stream return True # bars are on the stack (new and original)
測試文稿已經具備了使用它的功能:
$ ./weekdaysaligner.py --online --data0 '^GSPC' --data1 '^GDAXI' --filler
將--filler
添加到 WeekDaysFiller
和 data0
data1
中。輸出:
0001, True, data0, 2012-01-03T23:59:59, 2012-01-03T23:59:59, data1 ... 0009, True, data0, 2012-01-13T23:59:59, 2012-01-13T23:59:59, data1 0010, True, data0, 2012-01-16T23:59:59, 2012-01-16T23:59:59, data1 0011, True, data0, 2012-01-17T23:59:59, 2012-01-17T23:59:59, data1 ...
第1個 難題 - 2012-01-13
2012-01-17
已經消失了。並且整個集合是同步的:
... 0256, True, data0, 2012-12-25T23:59:59, 2012-12-25T23:59:59, data1 0257, True, data0, 2012-12-26T23:59:59, 2012-12-26T23:59:59, data1 0258, True, data0, 2012-12-27T23:59:59, 2012-12-27T23:59:59, data1 0259, True, data0, 2012-12-28T23:59:59, 2012-12-28T23:59:59, data1 0260, True, data0, 2012-12-31T23:59:59, 2012-12-31T23:59:59, data1
值得注意的事情:
-
正如我們
^GSPC
lines250
(指數交易250
日)2012
data0
-
data0
256
我們有^GDAXI
lines(指數交易256
日)2012
-
隨著到位,
WeekDaysFiller
兩個數據的長度已擴展到260
添加
52
*2
(週末和週末中的幾天),我們最終會得到364
.在一年中正常365
日子之前的剩餘日子肯定是星期六或星期日。
篩選器填充NaN
給定數據未進行交易的天數的值。讓我們把它畫出來:
$ ./weekdaysaligner.py --online --data0 '^GSPC' --data1 '^GDAXI' --filler --plot
充滿的日子是顯而易見的:
-
柱線之間的間隙就在那裡
-
對於 volume 圖來說,差距更加明顯
第2個圖將嘗試回答頂部的問題:指標會發生什麼?請記住,新柱線的值已指定為 NaN
(這就是為什麼它們不顯示):
$ ./weekdaysaligner.py --online --data0 '^GSPC' --data1 '^GDAXI' --filler --plot --sma 10
重新起泡藤壺!簡單移動平均線打破了時空連續體,在沒有連續性解的情況下跳過了一些柱。這當然是用Not a Number(又名NaN
)填充的效果:數學運算不再有意義。
如果使用lastNaN
看到的收盤價:
$ ./weekdaysaligner.py --online --data0 '^GSPC' --data1 '^GDAXI' --filler --plot --sma 10 --fillclose
在整個260天內,使用常規SMA,該情節看起來要好得多。
結論
將兩種工具與不同的交易日歷同步是一個決策和妥協的問題。backtrader
需要時間一致的數據來處理多個數據,不同的交易日歷無濟於事。
使用這裡描述的WeekDaysFiller
可以緩解這種情況,但它絕不是普遍的靈丹妙藥,因為用哪些值來填補是一個長期和長期考慮的問題。
腳本代碼和用法
可作為以下來源backtrader
的樣品:
$ ./weekdaysaligner.py --help usage: weekdaysaligner.py [-h] [--online] --data0 DATA0 [--data1 DATA1] [--sma SMA] [--fillclose] [--filler] [--filler0] [--filler1] [--fromdate FROMDATE] [--todate TODATE] [--plot] Sample for aligning with trade optional arguments: -h, --help show this help message and exit --online Fetch data online from Yahoo (default: False) --data0 DATA0 Data 0 to be read in (default: None) --data1 DATA1 Data 1 to be read in (default: None) --sma SMA Add a sma to the datas (default: 0) --fillclose Fill with Close price instead of NaN (default: False) --filler Add Filler to Datas 0 and 1 (default: False) --filler0 Add Filler to Data 0 (default: False) --filler1 Add Filler to Data 1 (default: False) --fromdate FROMDATE, -f FROMDATE Starting date in YYYY-MM-DD format (default: 2012-01-01) --todate TODATE, -t TODATE Ending date in YYYY-MM-DD format (default: 2012-12-31) --plot Do plot (default: False)
代碼:
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import datetime import backtrader as bt import backtrader.feeds as btfeeds import backtrader.indicators as btind import backtrader.utils.flushfile # from wkdaysfiller import WeekDaysFiller from weekdaysfiller import WeekDaysFiller class St(bt.Strategy): params = (('sma', 0),) def __init__(self): if self.p.sma: btind.SMA(self.data0, period=self.p.sma) btind.SMA(self.data1, period=self.p.sma) def next(self): dtequal = (self.data0.datetime.datetime() == self.data1.datetime.datetime()) txt = '' txt += '%04d, %5s' % (len(self), str(dtequal)) txt += ', data0, %s' % self.data0.datetime.datetime().isoformat() txt += ', %s, data1' % self.data1.datetime.datetime().isoformat() print(txt) def runstrat(): args = parse_args() fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') cerebro = bt.Cerebro(stdstats=False) DataFeed = btfeeds.YahooFinanceCSVData if args.online: DataFeed = btfeeds.YahooFinanceData data0 = DataFeed(dataname=args.data0, fromdate=fromdate, todate=todate) if args.data1: data1 = DataFeed(dataname=args.data1, fromdate=fromdate, todate=todate) else: data1 = data0.clone() if args.filler or args.filler0: data0.addfilter(WeekDaysFiller, fillclose=args.fillclose) if args.filler or args.filler1: data1.addfilter(WeekDaysFiller, fillclose=args.fillclose) cerebro.adddata(data0) cerebro.adddata(data1) cerebro.addstrategy(St, sma=args.sma) cerebro.run(runonce=True, preload=True) if args.plot: cerebro.plot(style='bar') def parse_args(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Sample for aligning with trade ') parser.add_argument('--online', required=False, action='store_true', help='Fetch data online from Yahoo') parser.add_argument('--data0', required=True, help='Data 0 to be read in') parser.add_argument('--data1', required=False, help='Data 1 to be read in') parser.add_argument('--sma', required=False, default=0, type=int, help='Add a sma to the datas') parser.add_argument('--fillclose', required=False, action='store_true', help='Fill with Close price instead of NaN') parser.add_argument('--filler', required=False, action='store_true', help='Add Filler to Datas 0 and 1') parser.add_argument('--filler0', required=False, action='store_true', help='Add Filler to Data 0') parser.add_argument('--filler1', required=False, action='store_true', help='Add Filler to Data 1') parser.add_argument('--fromdate', '-f', default='2012-01-01', help='Starting date in YYYY-MM-DD format') parser.add_argument('--todate', '-t', default='2012-12-31', help='Ending date in YYYY-MM-DD format') parser.add_argument('--plot', required=False, action='store_true', help='Do plot') return parser.parse_args() if __name__ == '__main__': runstrat()