量化交易学习(五十一)backtrader多周期回测(1)多数据源

通常我们在回测时使用的是单一频率的数据,比如:日线、分钟线等。有些策略是需要结合多个周期的数据进行决策的。这个时候应该怎么办呢。

其实backtrader本身就支持多周期回测,在通常情况下,最简单的方式就是同时提供多个维度的数据源,backtrader会选择最先提供给它的数据源的时间周期作为策略执行的时间周期。

比如我有1分钟线和日线,想实现这样的一个日内择时的策略:若当前分钟线的收盘价小于前一天的日线最低价则买入,若当前分钟线的收盘价大于前一天的最高价则卖出,怎么做呢?

可以这样,把1分钟线和日线都添加到cerebro中,然后策略启动后,每次执行next方法时self.datas都会调用下一根分钟线和分钟线所在日期的日线,这样在next方法中只用考虑当前分钟线和前一天日线就可以了。

下面是具体的代码,用的恒生科技ETF2024年3月份的数据:
main.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import backtrader as bt

import strategy

cerebro = bt.Cerebro()

cerebro.addstrategy(strategy.MyStrategy)

data0=bt.feeds.BacktraderCSVData(dataname='SHSE.513180_1min.csv',timeframe=bt.TimeFrame.Minutes)
data1=bt.feeds.BacktraderCSVData(dataname='SHSE.513180_1day.csv',timeframe=bt.TimeFrame.Days)

cerebro.adddata(data0)
cerebro.adddata(data1)

cerebro.broker.setcash(100000)

print('初始时资金持仓:%.2f' % cerebro.broker.getvalue())
cerebro.run()
print('结束时资金持仓:%.2f' % cerebro.broker.getvalue())
cerebro.plot(style='candle',barup='red',bardown='green',volup='red',voldown='green',voloverlay=False,valuetags=False)

strategy.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import backtrader as bt

class MyStrategy(bt.Strategy):
# 打印日志的函数
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
bartim=bt.utils.num2date(self.datas[0].datetime[0])
print('%s, %s' % (bartim, txt))

# 策略初始化,设置一些参数
def __init__(self):
# 标第的收盘价
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(
'买入已执行,价格为:%.3f,花费:%.2f,佣金:%.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
else:
self.log(
'卖出已执行,价格为:%.3f,花费:%.2f,佣金:%.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('当前盈亏 %.2f ,去除佣金后的盈亏 %.2f' %
(trade.pnl, trade.pnlcomm))

# 每条k线都会执行这个函数
def next(self):
if self.order:
return
# 当前分钟线的收盘价小于前一天的日线最低价则买入
if self.data0.close[0] < self.data1.low[0]:
if not self.position:
vol = self.broker.getvalue()*0.9 / self.data0.close[0] // 100 * 100
self.log('创建买单,%.3f,量为:%d' % (self.data0.close[0] , vol))
self.order=self.buy(size=vol)
elif self.data0.close[0]>self.data1.high[0]:
if self.position:
self.log('创建卖单,%.3f,量为:%d' % (self.data0.close[0] , self.position.size))
self.order=self.sell(size=self.position.size)

分钟线数据:
6778300a0b43fb0dec6a4ab68afc006c.png
日线数据:
3f5e263ab101bb98a7302dfce25835b3.png

回测结果:

Snipaste_2024-04-17_22-41-20.png

Snipaste_2024-04-17_22-41-57.png

可以看到上方密集的图是分钟线,下方细条的是日线。为什么日线会变得这么细长呢?在多周期回测时,日线也会用分钟线的宽度表示,不知道这算不算bug。放大看是这样子的:

Snipaste_2024-04-17_22-45-03.png

在多周期回测时,长周期的k线只会在其所在的周期结束后才可用,也就是如果今天是3月5日的9点30分,则1分钟线self.data0用的是9点29分开始,9点30分结束的k线,而日线self.data1用的是3月4日的日线,因为3月5日还没过完。


这一篇就到这里啦。欢迎大家点赞、转发、私信。还没有关注我的朋友可以关注 江达小记

江达小记