背景:需要擴充data feeds的場景
在backtrader中,data feeds中包含了被普遍認為是業界标準的幾個字段:
- datetime
- open
- high
- low
- close
- volume
- openinterest
可以使用GenericCSVData讀取CSV檔案,來友善地加載這些資料。但是在很多情況下,還需要在回測架構中使用其他的資料,例如:
- 利用分類算法,預測出股票是否已經達到買點及賣點(轉化為分類問題),在backtrader中按照預測的買點及賣點進行交易,就需要将預測結果讀入到架構中進行回測。
- 利用序列預測算法,預測出模型每日的收盤價,将預測值與其他技術名額綜合分析,制定交易政策,然後進行回測,這也需要将預測得到的序列值讀入到架構中。
是以,我們要想辦法從CSV中讀入自定義的資料,來使得這些資料可以在政策中得以應用。本文就針對上面提到的第2個場景,來擴充data feeds,實作自定義資料在backtrader回測架構中的使用。
應用場景設定
我們假定這樣的應用場景:
- 已知某隻股票的曆史日線資料,包含datetime,open,high,low,close字段;
- 已經通過序列預測算法,利用曆史資料計算出每日收盤價(或開盤價)的預測值;
- 利用曆史日線資料及預測資料對該股票進行政策回測;
- 政策為:當收盤價小于前1日的預測值時,進行買入;當收盤價大于前1日的預測值時,進行賣出。
實作步驟
- 生成待讀入CSV檔案,檔案中包含datetime,open,high,low,close及自定義資料字段(這裡使用predict進行辨別)。由于這裡隻是做示意,是以簡單的使用predict = (high + low) / 2來計算predict的值。合并後CSV檔案截圖如下:
Python量化交易學習筆記(25)——Data Feeds擴充 - 建立GenericCSVData的子類,擴充新的lines對象,添加新的參數,這樣在使用這個子類時,調用者就可以通過這個參數,來指定自定義的資料在CSV檔案中的哪一列,代碼如下:
# 擴充DataFeed
class GenericCSVDataEx(GenericCSVData):
# 添加自定義line
lines = ('predict', )
# openinterest在GenericCSVData中的預設索引是7,這裡對自定義的line的索引加1,使用者可指定
params = (('predict', 8),)
這裡建立了一個GenericCSVData的子類GenericCSVDataEx,定義了一個新的lines對象predict,并且添加了一個新的參數predict,預設值設定為8,在後續調用時,根據自定義資料具體在檔案中的哪一列,再對這個參數進行重新指派。
- 使用建立的子類導入資料,代碼如下:
# 建立資料
data = GenericCSVDataEx(
dataname = datapath,
fromdate = datetime.datetime(2019, 1, 1),
todate = datetime.datetime(2019, 12, 31),
nullvalue = 0.0,
dtformat = ('%Y/%m/%d'),
datetime = 0,
open = 1,
high = 2,
low = 3,
close = 4,
volume = -1,
openinterest = -1,
predict = 5
)
這裡使用了新建立的類GenericCSVDataEx來導入資料。在參數中,從datetime開始,等号後面的值均表示對應字段在CSV檔案中的列号,-1表示檔案沒有該字段資料,可以回看前面的CSV檔案的截圖,确認一下資料的對應關系。
- 在Strategy中使用自定義字段資料,主要代碼如下:
class TestStrategy(bt.Strategy):
params = (
# 要跳過的K線根數
('skip_len', 1),
)
def __init__(self):
# 引用data[0]資料的收盤價資料
self.dataclose = self.datas[0].close
self.datapredict = self.datas[0].predict
# 用于繪制predict曲線
btind.SMA(self.data.predict, period = 1, subplot = False)
# 用于記錄訂單狀态
self.order = None
self.buyprice = None
self.buycomm = None
def next(self):
# 因為需要在政策中需要和前一日的預測值比較,是以要跳過第1根K線
if (len(self) <= self.p.skip_len):
return
# 檢查是否有訂單等待處理,如果是就不再進行其他下單
if self.order:
return
# 檢查是否已經進場
if not self.position:
# 還未進場,則隻能進行買入
# 當日收盤價小于前一日預測價
if self.dataclose[0] < self.datapredict[-1]:
# 買買買
# 記錄訂單避免二次下單
self.order = self.buy()
# 如果已經在場内,則可以進行賣出操作
else:
# 當日收盤價大于前一日預測價
if self.dataclose[0] > self.datapredict[-1]:
# 賣賣賣
# 記錄訂單避免二次下單
self.order = self.sell()
在init函數中,直接使用self.datas[0].predict就可以通路到我們自定義的predict字段的值,然後将它儲存在執行個體變量self.datapredict中,這樣就可以在next函數中進行使用了。
self.datapredict = self.datas[0].predict
在next函數中實作政策:當收盤價小于前1日的預測值時,進行買入;當收盤價大于前1日的預測值時,進行賣出。
if not self.position:
if self.dataclose[0] < self.datapredict[-1]:
self.order = self.buy()
else:
if self.dataclose[0] > self.datapredict[-1]:
self.order = self.sell()
需要說明以下幾點:
- 這裡的self.datapredict(self.datas[0].predict)是lines對象,在next函數中使用時,索引[0]表示當日的資料,索引[-1]表示前1日的資料
- 由于要和前1日predict的值做比較,而對于第1個交易日而言,是沒有前1日predict值的,是以在next函數中,使用下面的代碼跳過1根K線
# 因為需要在政策中需要和前一日的預測值比較,是以要跳過第1根K線
if (len(self) <= self.p.skip_len):
return
- backtrader沒有提供自定義的資料繪制功能,可以在init函數中,通過借用單日的簡單移動平均線來繪制自定義資料的曲線
# 用于繪制predict曲線
btind.SMA(self.data.predict, period = 1, subplot = False)
回測結果如下圖所示:
Data Feeds擴充代碼:
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime # 用于datetime對象操作
import os.path # 用于管理路徑
import sys # 用于在argvTo[0]中找到腳本名稱
import backtrader as bt # 引入backtrader架構
from backtrader.feeds import GenericCSVData # 用于擴充DataFeed
import backtrader.indicators as btind
# 擴充DataFeed
class GenericCSVDataEx(GenericCSVData):
# 添加自定義line
lines = ('predict', )
# openinterest在GenericCSVData中的預設索引是7,這裡對自定義的line的索引加1,使用者可指定
params = (('predict', 8),)
# 建立政策
class TestStrategy(bt.Strategy):
params = (
# 要跳過的K線根數
('skip_len', 1),
)
def log(self, txt, dt=None):
''' 政策的日志函數'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# 引用data[0]資料的收盤價資料
self.dataclose = self.datas[0].close
self.datapredict = self.datas[0].predict
# 用于繪制predict曲線
btind.SMA(self.data.predict, period = 1, subplot = False)
# 用于記錄訂單狀态
self.order = None
self.buyprice = None
self.buycomm = None
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# 送出給代理或者由代理接收的買/賣訂單 - 不做操作
return
# 檢查訂單是否執行完畢
# 注意:如果沒有足夠資金,代理會拒絕訂單
if order.status in [order.Completed]:
if order.isbuy():
self.log(
'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
else: # 賣
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
# 無等待處理訂單
self.order = None
def notify_trade(self, trade):
if not trade.isclosed:
return
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(trade.pnl, trade.pnlcomm))
def next(self):
# 因為需要在政策中需要和前一日的預測值比較,是以要跳過第1根K線
if (len(self) <= self.p.skip_len):
return
# 日志輸出收盤價資料
self.log('Close, %.2f' % self.dataclose[0])
# 檢查是否有訂單等待處理,如果是就不再進行其他下單
if self.order:
return
# 檢查是否已經進場
if not self.position:
# 還未進場,則隻能進行買入
# 當日收盤價小于前一日預測價
if self.dataclose[0] < self.datapredict[-1]:
# 買買買
# 記錄訂單避免二次下單
self.log('BUY CREATE, %.2f' % self.dataclose[0])
self.order = self.buy()
# 如果已經在場内,則可以進行賣出操作
else:
# 當日收盤價大于前一日預測價
if self.dataclose[0] > self.datapredict[-1]:
# 賣賣賣
# 記錄訂單避免二次下單
self.log('SELL CREATE, %.2f' % self.dataclose[0])
self.order = self.sell()
# 建立cerebro實體
cerebro = bt.Cerebro()
# 添加政策
cerebro.addstrategy(TestStrategy)
# 先找到腳本的位置,然後根據腳本與資料的相對路徑關系找到資料位置
# 這樣腳本從任意地方被調用,都可以正确地通路到資料
modpath = os.path.dirname(os.path.abspath(sys.argv[0]))
datapath = os.path.join(modpath, './custom.csv')
# 建立資料
data = GenericCSVDataEx(
dataname = datapath,
fromdate = datetime.datetime(2019, 1, 1),
todate = datetime.datetime(2019, 12, 31),
nullvalue = 0.0,
dtformat = ('%Y/%m/%d'),
datetime = 0,
open = 1,
high = 2,
low = 3,
close = 4,
volume = -1,
openinterest = -1,
predict = 5
)
# 在Cerebro中添加價格資料
cerebro.adddata(data)
# 設定啟動資金
cerebro.broker.setcash(100000.0)
# 設定交易機關大小
cerebro.addsizer(bt.sizers.FixedSize, stake = 100)
# 設定傭金為千分之一
cerebro.broker.setcommission(commission=0.001)
# 列印開始資訊
print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
# 周遊所有資料
cerebro.run()
# 列印最後結果
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.plot()
為了便于互相交流學習,建立了微信群,感興趣的讀者請加微信。