天天看點

Python量化交易學習筆記(25)——Data Feeds擴充

背景:需要擴充data feeds的場景

在backtrader中,data feeds中包含了被普遍認為是業界标準的幾個字段:

  • datetime
  • open
  • high
  • low
  • close
  • volume
  • openinterest

可以使用GenericCSVData讀取CSV檔案,來友善地加載這些資料。但是在很多情況下,還需要在回測架構中使用其他的資料,例如:

  1. 利用分類算法,預測出股票是否已經達到買點及賣點(轉化為分類問題),在backtrader中按照預測的買點及賣點進行交易,就需要将預測結果讀入到架構中進行回測。
  2. 利用序列預測算法,預測出模型每日的收盤價,将預測值與其他技術名額綜合分析,制定交易政策,然後進行回測,這也需要将預測得到的序列值讀入到架構中。

是以,我們要想辦法從CSV中讀入自定義的資料,來使得這些資料可以在政策中得以應用。本文就針對上面提到的第2個場景,來擴充data feeds,實作自定義資料在backtrader回測架構中的使用。

應用場景設定

我們假定這樣的應用場景:

  1. 已知某隻股票的曆史日線資料,包含datetime,open,high,low,close字段;
  2. 已經通過序列預測算法,利用曆史資料計算出每日收盤價(或開盤價)的預測值;
  3. 利用曆史日線資料及預測資料對該股票進行政策回測;
  4. 政策為:當收盤價小于前1日的預測值時,進行買入;當收盤價大于前1日的預測值時,進行賣出。

實作步驟

  1. 生成待讀入CSV檔案,檔案中包含datetime,open,high,low,close及自定義資料字段(這裡使用predict進行辨別)。由于這裡隻是做示意,是以簡單的使用predict = (high + low) / 2來計算predict的值。合并後CSV檔案截圖如下:
    Python量化交易學習筆記(25)——Data Feeds擴充
  2. 建立GenericCSVData的子類,擴充新的lines對象,添加新的參數,這樣在使用這個子類時,調用者就可以通過這個參數,來指定自定義的資料在CSV檔案中的哪一列,代碼如下:
# 擴充DataFeed
class GenericCSVDataEx(GenericCSVData):
    # 添加自定義line
    lines = ('predict', )
    # openinterest在GenericCSVData中的預設索引是7,這裡對自定義的line的索引加1,使用者可指定
    params = (('predict', 8),)
           

這裡建立了一個GenericCSVData的子類GenericCSVDataEx,定義了一個新的lines對象predict,并且添加了一個新的參數predict,預設值設定為8,在後續調用時,根據自定義資料具體在檔案中的哪一列,再對這個參數進行重新指派。

  1. 使用建立的子類導入資料,代碼如下:
# 建立資料
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檔案的截圖,确認一下資料的對應關系。

  1. 在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)
           

回測結果如下圖所示:

Python量化交易學習筆記(25)——Data Feeds擴充

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()
           

為了便于互相交流學習,建立了微信群,感興趣的讀者請加微信。

Python量化交易學習筆記(25)——Data Feeds擴充