天天看點

Python量化交易學習筆記(38)——backtrader多周期回測2

簡述

如果需要做多周期的政策回測,但是隻有單周期資料可用,那麼就可以使用重采樣(resampling)來解決多周期資料的生成問題。

這裡的重采樣(resampling)實際指的是上采樣(upsampling),使用小周期的資料來合成大周期資料。例如,用日線資料合成周線資料。這裡所說的上采樣和信号處理的上采樣效果是相反的,在信号進行中,上采樣會獲得比源資料更多的資料,而這裡的上采樣則是獲得大周期的資料,較源資料而言,資料量變少。

backtrader的重采樣

backtrader内置了重采樣方法:

cerebro.resampledata(data, **kwargs)      

其中data為源資料(小周期資料),通過該方法調用,重采樣後的目标資料(大周期資料)就已經被添加到cerebro中。

該方法主要有兩個參數來控制重采樣的具體實作:

  • timeframe(預設值:bt.TimeFrame.Days)

該參數為目标資料的周期類型,目标資料周期應大于等于源資料,即如果源資料為日線資料,那麼目标資料可以為日線、周線、月線或更大周期資料。

  • compression(預設值:1)

該參數為壓縮比,取值為1到n,将目标資料的n根K線進一步壓縮成1根。

使用resample時,同樣也​​筆記(37)​​存在中提到的,大周期資料的使用會使得政策的最小周期變大的情況。

樞軸點(Pivot Point)

樞軸點本身就是一個多周期的技術名額,本文示例會用到Pivot Point名額,是以這裡做一下簡單介紹。

樞軸點名額考慮的是過去時間内更大周期的K線取值情況,例如,當對日線進行研判時,會使用上一個月的月線資料進行名額計算。當然也可以根據需要使用日線與其他大周期K線進行組合使用。

名額的計算公式為:

  • pivot = (high + low + close) / 3
  • support1 = 2.0 * pivot - high
  • support2 = pivot - (high - low)
  • resistance1 = 2.0 * pivot - low
  • resistance2 = pivot + (high - low)

仍以日線與月線的組合為例,這裡的high、low、close分别對應于上月月線的最高價、最低價、收盤價。

在backtrader中,這5個名額對應的名稱為p、s1、s2、r1、r2。其中p為軸心點,s1、s2對應第1、2級支撐,r1、r2對應于第1、2級阻力。

示例

為了示範resample回測,本文使用以下方案:

  • 政策将先讀入日線資料,然後通過resample生成并加載月線資料。回測股票為000001平安銀行,回測周期為2018年1月1日至2019年12月31日。
# 加載資料
def load_data(stk_code, fromdate, todate):
    datapath = './stk_data/d/' + stk_code + '.csv'
    return bt.feeds.GenericCSVData(
        dataname = datapath,
        fromdate = fromdate,
        todate = todate + datetime.timedelta(days=1),
        nullvalue = 0.0,
        dtformat = ('%Y-%m-%d'),
        datetime = 0,
        open = 1,
        high = 2,
        low = 3,
        close = 4,
        volume = 5,
        openinterest = -1
        )

stk_list = ['sz.000001']
fromdate = datetime.datetime(2018, 1, 1)
todate = datetime.datetime(2019, 12, 31)
for stk_code in stk_list:
    data = load_data(stk_code, fromdate, todate)
    cerebro.adddata(data)
    cerebro.resampledata(data, timeframe = bt.TimeFrame.Months, compression = 1)      

先加載日線資料,然後通過resample得到并加載月線資料。

  • 買入條件:價格回踩2級支撐或突破1級阻力;賣出條件:價格較最高收盤價回撤5%賣出。

在政策類的init方法中,定義所需的技術名額:

def __init__(self):
        # 存儲不同資料的技術名額
        self.inds = dict()
        # 存儲特定股票的訂單,key為股票的代碼
        self.orders = dict()
        # 周遊所有資料
        for i, d in enumerate(self.datas):
            self.orders[d._name] = None
            # 為每個資料定義字典,存儲技術名額
            self.inds[d] = dict()
            # 判斷d是否為日線資料
            if 0 == i % 2:
                self.inds[d]['lowest'] = btind.Lowest(d, period = self.p.lowestperiod)
            # 判斷d是否為月線資料
            else:
                # 定義pivot point名額
                self.inds[d]['pp'] = btind.PivotPoint(d)      

定義字典self.inds,來存儲不同資料的技術名額。

定義self.orders,來存儲特定股票的訂單,key為股票的代碼。

然後周遊所有的資料,将資料的訂單先置為空,并且為每個資料建立字典,來存儲技術名額。由于系統是先添加日線資料,再添加月線資料,是以當i % 2等于0時,d為日線資料,那麼就計算最小值名額;當i % 2不等于0時,d為月線資料,那麼計算Pivot Point名額。

在政策類的next方法中,定義買入賣出條件:

def next(self):
        for i, d in enumerate(self.datas):
            # 如果處理月線資料則跳過買賣條件,因為已在日線資料判斷處理過
            if 1 == i % 2:
                continue
            pos = self.getposition(d)
            # 不在場内,則可以買入
            if not len(pos):
                # 達到買入條件
                month_pp = self.inds[self.datas[i + 1]]['pp']
                if (self.inds[d]['lowest'] <= month_pp.s2 and d.close > month_pp.s2) or (
                    self.inds[d]['lowest'] <= month_pp.r1 and d.close > month_pp.r1) :
                    # 買入手數
                    stake = int(self.broker.cash / len(stk_list) // (d.close[0] * 100)) * 100
                    # 買買買
                    self.buy(data = d, size = stake)
            elif not self.orders[d._name]:
                # 下保護點賣單
                self.orders[d._name] = self.close(data = d, exectype= bt.Order.StopTrail,
                            trailamount=self.p.trailamount,
                            trailpercent=self.p.trailpercent)      

這裡依然隻對i % 2等于0的資料(即日線資料)進行條件判斷,然後使用self.inds[self.datas[i + 1]][‘pp’]的方式對月線技術名額進行通路。

當達到買入條件後下買單,訂單将在第二天以開盤價成交。在計算買入倉位大小時,保證資金得到最大程度的使用。

買單成交當天,下StopTrail賣單,當股價較最高收盤價回撤5%賣出(具體參加​​筆記(20)​​​和​​筆記(31)​​)。這裡使用close方法而不是sell方法,如果使用sell方法,股票将以1股1股的賣出,使用close則是全部賣出。

輸出結果為:

2018-08-27 BUY sz.000001 EXECUTED, Price: 10.02
2018-09-10 SELL sz.000001 EXECUTED, Price: 9.91
2018-10-08 BUY sz.000001 EXECUTED, Price: 10.70
2018-10-11 SELL sz.000001 EXECUTED, Price: 10.03
2019-01-21 BUY sz.000001 EXECUTED, Price: 10.34
2019-03-08 SELL sz.000001 EXECUTED, Price: 12.43
2019-04-09 BUY sz.000001 EXECUTED, Price: 13.87
2019-04-23 SELL sz.000001 EXECUTED, Price: 13.99
2019-06-21 BUY sz.000001 EXECUTED, Price: 13.76
2019-07-08 SELL sz.000001 EXECUTED, Price: 13.47
2019-08-13 BUY sz.000001 EXECUTED, Price: 15.00
2019-08-22 SELL sz.000001 EXECUTED, Price: 14.24
2019-08-26 BUY sz.000001 EXECUTED, Price: 14.42
2019-10-22 SELL sz.000001 EXECUTED, Price: 16.36
Final Portfolio Value: 1184108.05      
Python量化交易學習筆記(38)——backtrader多周期回測2

兩年間共有7筆交易,收益率為18.4%

總結

  • backtrader可以通過resample來實作基于單周期資料的多周期資料生成與加載。
  • 使用源資料進行resample時,目标資料周期應大于等于源資料周期
  • 在政策實作時,通過資料索引的取餘運算來區分源資料、目标資料。

多周期政策回測程式v2代碼:

# 多周期
# 買入條件:價格回踩2級支撐或突破1級阻力
# 賣出條件:價格較最高收盤價回撤5%賣出
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind
import datetime
import pandas as pd


stk_list = ['sz.000001']

class PivotMultiTF(bt.Strategy):
    params = (
        ('lowestperiod', 5),
        ('trailamount', 0.0),
        ('trailpercent', 0.05),
    )

    def __init__(self):
        # 存儲不同資料的技術名額
        self.inds = dict()
        # 存儲特定股票的訂單,key為股票的代碼
        self.orders = dict()
        # 周遊所有資料
        for i, d in enumerate(self.datas):
            self.orders[d._name] = None
            # 為每個資料定義字典,存儲技術名額
            self.inds[d] = dict()
            # 判斷d是否為日線資料
            if 0 == i % 2:
                self.inds[d]['lowest'] = btind.Lowest(d, period = self.p.lowestperiod)
            # 判斷d是否為月線資料
            else:
                # 定義pivot point名額
                self.inds[d]['pp'] = btind.PivotPoint(d)

    def next(self):
        for i, d in enumerate(self.datas):
            # 如果處理月線資料則跳過買賣條件,因為已在日線資料判斷處理過
            if 1 == i % 2:
                continue
            pos = self.getposition(d)
            # 不在場内,則可以買入
            if not len(pos):
                # 達到買入條件
                month_pp = self.inds[self.datas[i + 1]]['pp']
                if (self.inds[d]['lowest'] <= month_pp.s2 and d.close > month_pp.s2) or (
                    self.inds[d]['lowest'] <= month_pp.r1 and d.close > month_pp.r1) :
                    # 買入手數
                    stake = int(self.broker.cash / len(stk_list) // (d.close[0] * 100)) * 100
                    # 買買買
                    self.buy(data = d, size = stake)
            elif not self.orders[d._name]:
                # 下保護點賣單
                self.orders[d._name] = self.close(data = d, exectype= bt.Order.StopTrail,
                            trailamount=self.p.trailamount,
                            trailpercent=self.p.trailpercent)

    def notify_order(self, order):
        if order.status in [order.Completed]:
            if order.isbuy():
                print('{} BUY {} EXECUTED, Price: {:.2f}'.format(self.datetime.date(), order.data._name, order.executed.price))
            else:  # Sell
                self.orders[order.data._name] = None
                print('{} SELL {} EXECUTED, Price: {:.2f}'.format(self.datetime.date(), order.data._name, order.executed.price))

# 加載資料
def load_data(stk_code, fromdate, todate):
    datapath = './stk_data/d/' + stk_code + '.csv'
    return bt.feeds.GenericCSVData(
        dataname = datapath,
        fromdate = fromdate,
        todate = todate + datetime.timedelta(days=1),
        nullvalue = 0.0,
        dtformat = ('%Y-%m-%d'),
        datetime = 0,
        open = 1,
        high = 2,
        low = 3,
        close = 4,
        volume = 5,
        openinterest = -1
        )

def runstrat():
    cerebro = bt.Cerebro()
    cerebro.broker.setcash(1000000.0)
    cerebro.addstrategy(PivotMultiTF)
    fromdate = datetime.datetime(2018, 1, 1)
    todate = datetime.datetime(2019, 12, 31)
    for stk_code in stk_list:
        data = load_data(stk_code, fromdate, todate)
        cerebro.adddata(data)
        cerebro.resampledata(data, timeframe = bt.TimeFrame.Months, compression = 1)
    cerebro.addwriter(bt.WriterFile, out = 'log.csv', csv = True)
    cerebro.run()
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
    # Plot the result繪制結果
    cerebro.plot(start=datetime.date(2018, 1, 1), end=datetime.date(2019, 12, 31),
            volume = False, style = 'candle',
            barup = 'red', bardown = 'green')

if __name__ == '__main__':
    runstrat()