本文继续介绍backtrader多周期回测的第二种方法,使用resample来进行多周期数据的加载。
如果需要做多周期的策略回测,但是只有单周期数据可用,那么就可以使用重采样(resampling)来解决多周期数据的生成问题。
这里的重采样(resampling)实际指的是上采样(upsampling),使用小周期的数据来合成大周期数据。例如,用日线数据合成周线数据。这里所说的上采样和信号处理的上采样效果是相反的,在信号处理中,上采样会获得比源数据更多的数据,而这里的上采样则是获得大周期的数据,较源数据而言,数据量变少。
backtrader内置了重采样方法:
cerebro.resampledata(data, **kwargs)
其中data为源数据(小周期数据),通过该方法调用,重采样后的目标数据(大周期数据)就已经被添加到cerebro中。
该方法主要有两个参数来控制重采样的具体实现:
该参数为目标数据的周期类型,目标数据周期应大于等于源数据,即如果源数据为日线数据,那么目标数据可以为日线、周线、月线或更大周期数据。
该参数为压缩比,取值为1到n,将目标数据的n根K线进一步压缩成1根。
使用resample时,同样也笔记(37)存在中提到的,大周期数据的使用会使得策略的最小周期变大的情况。
枢轴点本身就是一个多周期的技术指标,本文示例会用到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级阻力。
更多枢轴点的信息可参考:
https://school.stockcharts.com/doku.php?id=technical_indicators:pivot_points
为了演示resample回测,本文使用以下方案:
# 加载数据
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得到并加载月线数据。
在策略类的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
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()