Python回测神器Backtrader (下)

目录
  1. 1. 10.使用Backtrader回测
    1. 1.1. 双均线策略
    2. 1.2. 数据划分
    3. 1.3. 双均线策略的实现
    4. 1.4. 订单相关
    5. 1.5. 交易逻辑
    6. 1.6. 如何使用内置的交叉提示工具
  2. 2. 11.使用Backtrader优化策略
  3. 3. 12.使用Backtrader选股
    1. 3.1. Analyzer类
  4. 4. 13.在Backtrader中编写技术指标
  5. 5. 14.在Backtrader中绘图
    1. 5.1. 对单只股票数据进行可视化
    2. 5.2. 对多只股票数据进行可视化
    3. 5.3. 将技术指标添加到图中
  6. 6. 15.使用另类数据
  7. 7. 16.其他补充的
  8. 8. 17.源码下载

在上一篇的基础上,本篇主要讲讲如何在Backtrader中进行回测、选股、优化及可视化,并给出例子中的源码。

10.使用Backtrader回测

选择量化研究中的Hello World策略进行介绍,即经典但不实用的 双均线策略

双均线策略

双均线策略,顾名思义,就是两根均线:短期均线和长期均线。当短线均线上穿长期均线(金叉)时买入,当短期均线下穿长期均线(死叉)时卖出,这就是双均线策略的核心思想。

在深入研究该策略前,首先专门写一个strategies.py文件容纳自己的策略。目的是将策略与主脚本分开,保证代码结构清晰。基本上,所有与Cerebro引擎有关的脚本在整个教程中只有微小的变化,大部分变化发生在与策略相关的strategies.py文件中。

1
2
3
4
5
6
7
8
9
10
11
12
class PrintClose(bt.Strategy):
def __init__(self):
##引用data[0]中的收盘价格数据
self.dataclose = self.datas[0].close

def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt)) #Print date and close

def next(self):
#将收盘价保留两位小数再输出
self.log('Close: %.2f' % self.dataclose[0])

数据划分

在进行测试时,需将数据分为“样本内数据”或“样本外数据”。程序可以针对样本内数据进行回测,策略优化(参数调整),最终在样本外数据上分析采用优化后的参数的策略的有效性。

对样本内外的数据,程序设置了不同的起始日期。日期的设置采用了DateTime模块。更新后的主脚本btmain.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
import datetime
import backtrader as bt
from strategies import *

cerebro = bt.Cerebro()

#给原始数据设置起止时间参数,并添加给Cerebro引擎
data = bt.feeds.YahooFinanceCSVData(
dataname='TSLA.csv',
fromdate=datetime.datetime(2016, 1, 1),
todate=datetime.datetime(2017, 12, 25))
#样本外数据的参数设置如下
#fromdate=datetime.datetime(2018, 1, 1),
#todate=datetime.datetime(2019, 12, 25))

cerebro.adddata(data)

#给Cerebro引擎添加策略
cerebro.addstrategy(MAcrossover)

#默认头寸大小
cerebro.addsizer(bt.sizers.SizerFix, stake=3)

if __name__ == '__main__':
#运行Cerebro引擎
start_portfolio_value = cerebro.broker.getvalue()

cerebro.run()

end_portfolio_value = cerebro.broker.getvalue()
pnl = end_portfolio_value - start_portfolio_value
print('Starting Portfolio Value: %.2f' % start_portfolio_value)
print('Final Portfolio Value: %.2f' % end_portfolio_value)
print('PnL: %.2f' % pnl)

从Strategies.py文件中import *,这便于调用该文件中的所有类。addsizer设置了默认头寸大小为3股。

Cerebro.broker.getvalue()命令可获取投资组合的当前金额。在运行Cerebro之前调用该函数获取初始本金,在策略运行完毕后获得投资组合的最终金额。终值扣除起始值即可得到损益。

双均线策略的实现

接下来定义Strategy的子类MACrossover类,代表双均线策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MAcrossover(bt.Strategy): 
#移动平均参数
params = (('pfast',20),('pslow',50),)
def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt)) # 执行策略优化时 可注释掉此行
def __init__(self):
self.dataclose = self.datas[0].close
# Order变量包含持仓数据与状态
self.order = None
# 初始化移动平均数据
self.slow_sma = bt.indicators.MovingAverageSimple(self.datas[0],
period=self.params.pslow)
self.fast_sma = bt.indicators.MovingAverageSimple(self.datas[0],
period=self.params.pfast)

这里将快慢周期设置为参数pfast和pslow而不是硬编码(固定值),以便后续对策略参数的优化。

__init__()函数下有一些新的变量,self.order变量存储正在执行的订单详细信息和订单状态,以便确定当前是否存在交易或是否有待处理的订单。

基于Backtrader内置的MovingAverageSimple命令计算了两个周期的简单移动平均价格。

需要指出的是,在Backtrader中,为了避免look-ahead偏差,当程序发出买入或卖出信号引导程序创建订单时,无论价格如何,该订单要到下一个k线被调用时才会执行。同时,在回测过程中,只有在指标值计算完毕后,才会开始寻找订单。

两个移动平均值中较大的时间周期采用最近50个收盘价的平均值。这意味着前50个数据点的移动平均值为NaN。在拥有有效的移动平均数据之前,Backtrader不会尝试创建订单。

订单相关

与交易相关的所有内容都发生在notify_order()函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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, %.2f' % order.executed.price)
elif order.issell():
self.log('SELL EXECUTED, %.2f' % order.executed.price)
self.bar_executed = len(self)
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
#重置订单
self.order = None

上述代码记录了执行订单的时间和价格。若订单未成交,此部分还将提供通知。

交易逻辑

Strategy类中的next()函数包含所有的交易逻辑。

这里,首先检查目前是否有持仓,有则不开仓。如果没有持仓,则可以在市场中寻找开仓信号,检查对于上一根K线SMA20移动平均线是否在SMA50移动平均线以下,但对于当前K线,SMA20移动平均线位于SMA50移动平均线以上,如果是则说明快线突破了慢线(金叉)。反之则快线跌破了慢线(死叉)。在获得开仓信号前,程序会不断确认是否存在开仓信号。如果持仓,则择机平仓。这里采用的退出策略为持仓5日。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def next(self):
# 检测是否有未完成订单
if self.order:
return
#验证是否有持仓
if not self.position:
#如果没有持仓,寻找开仓信号
#SMA快线突破SMA慢线
if self.fast_sma[0] > self.slow_sma[0] and self.fast_sma[-1] < self.slow_sma[-1]:
self.log('BUY CREATE, %.2f' % self.dataclose[0])
#继续追踪已经创建的订单,避免重复开仓
self.order = self.buy()
#如果SMA快线跌破SMA慢线
elif self.fast_sma[0] < self.slow_sma[0] and self.fast_sma[-1] > self.slow_sma[-1]:
self.log('SELL CREATE, %.2f' % self.dataclose[0])
#继续追踪已经创建的订单,避免重复开仓
self.order = self.sell()
else:
# 如果已有持仓,寻找平仓信号
if len(self) >= (self.bar_executed + 5):
self.log('CLOSE CREATE, %.2f' % self.dataclose[0])
self.order = self.close()

代码运行过程中会输出所有交易,并打印最终盈亏数据。在这种情况下,该策略获得了79美元的利润。

测试策略时要记住的一件事是,回测结束时有可能还存在持仓。检查是否有未平仓交易的一种方法是确保打印投资组合值之前的倒数第二行打印了“CLOSE CREATE”。否则,未平仓交易可能会扭曲盈亏表现。

如何使用内置的交叉提示工具

在双均线策略中,next()函数实现了对均线交叉的判断。Backtrader自带的CrossOver()函数可以简化这一过程,使用该功能时需在_init_()函数中将其初始化,如下所示:

1
self.crossover = bt.indicators.CrossOver(self.slow_sma,self.fast_sma)

然后,程序会自动确认是否有开平仓信号发出:

1
2
3
4
if self.crossover > 0: # Fast ma crosses above slow ma
pass # 开仓信号
elif self.crossover < 0: # Fast ma crosses below slow ma
pass # 平仓信号

11.使用Backtrader优化策略

尝试通过优化策略的参数(快慢线周期)来改善策略的某项指标(夏普比率)。

由于策略优化涉及大量的参数组合,并且保留所有交易记录对策略优化意义不大,因此在log()函数中注释掉打印语句。这里以夏普比率来判断策略的优劣,修改后的主程序如下:

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
import datetime
import backtrader as bt
from strategies import *

cerebro = bt.Cerebro(optreturn=False)

#设置数据的参数
data = bt.feeds.YahooFinanceCSVData(
dataname='TSLA.csv',
fromdate=datetime.datetime(2016, 1, 1),
todate=datetime.datetime(2017, 12, 25))
#样本外数据的设置
#fromdate=datetime.datetime(2018, 1, 1),
#todate=datetime.datetime(2019, 12, 25))

cerebro.adddata(data)

#向Cerebro引擎添加数据
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe_ratio')
cerebro.optstrategy(MAcrossover, pfast=range(5, 20), pslow=range(50, 100))

#设置头寸参数
cerebro.addsizer(bt.sizers.SizerFix, stake=3)

if __name__ == '__main__':
optimized_runs = cerebro.run()

final_results_list = []
for run in optimized_runs:
for strategy in run:
PnL = round(strategy.broker.get_value() - 10000,2)
sharpe = strategy.analyzers.sharpe_ratio.get_analysis()
final_results_list.append([strategy.params.pfast,
strategy.params.pslow, PnL, sharpe['sharperatio']])

sort_by_sharpe = sorted(final_results_list, key=lambda x: x[3],
reverse=True)
for line in sort_by_sharpe[:5]:
print(line)

代码的主要变化如下:

  1. 当初始化Cerebro引擎时,将optreturn参数设置为False,即只要求输出策略的参数以及analyzer对策略运行结果的统计,来提高运行速度。

  2. 添加了一个analyzer类的对象分析不同参数组合策略的夏普比率。

  3. 移除了cerebro.addstrategy(),取而代之的是cerebro.optstrategy(),表明要对该策略进行优化,并限制了待优化参数的取值范围。

最终,优化结果存储在多个列表构成的列表对象optimized_runs中。遍历该列表并将快慢线的周期数据及相应夏普比率汇总并排序,得到最终结果为

当快线周期取7,慢线周期取92时,结果最优。现在开始对样本外数据进行分析,此时需要修改数据的起止日期。

1
2
fromdate=datetime.datetime(201811),
todate=datetime.datetime(20191225))

对于样本外数据,采用优化前和优化后的策略参数的盈亏分别为亏损63.42美元和170.22美元,这一结果并不令人意外,这是因为:

  • 均线策略过于简单,是个难以盈利的策略。反而是第一次运行产生了利润让人比较意外;

  • 对样本内的数据进行优化导致了过拟合,故而在样本内数据取得了优异的结果,但在样本外数据上表现较差。

12.使用Backtrader选股

Backtrader可在给定的时期,按用户提出的准则选股。

这里将依据布林带准则选股,即选出交易价格比前20日均价低于两个标准差的股票。

Analyzer类

选股从创建Backtrader的子类Analyzer类开始,该类是选股的关键工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Screener_SMA(bt.Analyzer):
params = (('period',20), ('devfactor',2),)

def start(self):
self.bband = {data: bt.indicators.BollingerBands(data,
period=self.params.period, devfactor=self.params.devfactor)
for data in self.datas}

def stop(self):
self.rets['over'] = list()
self.rets['under'] = list()

for data, band in self.bband.items():
node = data._name, data.close[0], round(band.lines.bot[0], 2)
if data > band.lines.bot:
self.rets['over'].append(node)
else:
self.rets['under'].append(node)

start()函数中针对多个数据Feed计算了布林带参数,包括对应的上中下轨值。stop()函数实现了选股逻辑:遍历所有数据Feed,根据价格与布林带下轨的关系将股票分类。

所有的Analyzer类具有一个内置字典rets,这里使用rets的key over和under分别存储交易价格在布林带下轨上方和下方的标的。

主函数中instruments列表包含了待筛选的股票池,通过循环将相应股票的CSV数据文件添加到Cerebro引擎。

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

# 初始化Cerebro引擎
cerebro = bt.Cerebro()

# 建立股票池并将所有数据添加至Cerebro引擎
instruments = ['TSLA', 'AAPL', 'GE', 'GRPN']
for ticker in instruments:
data = bt.feeds.YahooFinanceCSVData(
dataname='{}.csv'.format(ticker),
fromdate=datetime.datetime(2016, 1, 1),
todate=datetime.datetime(2017, 10, 30))
cerebro.adddata(data)

# 添加基于布林带的选股器
cerebro.addanalyzer(Screener_SMA)

if __name__ == '__main__':
# 运行Cerebro引擎
cerebro.run(runonce=False, stdstats=False, writer=True)

接下来将新创建的screener类添加到Cerebro引擎作为分析器(Analyzer),并附加一些参数来调用Cerebro.run()命令。其中 writer = True参数调用内置的输出显示功能。stdstats = False会删除一些标准输出(见后绘图部分)。最后,runonce = False确保了数据的同步性。最终打印结果如下:

布林带策略的选股结果

13.在Backtrader中编写技术指标

有三种方法可以在Backtrader中编写指标。

  • 自行编写技术指标;

  • 使用内置技术指标;

  • 使用第三方库。

下面是自定义指标的一个例子,可理解成简化版的ATR(偷个懒)。在Backtrader中,通过使用负索引遍历最后14个数据点,取每个周期的高点并减去低点,然后将其平均,从而计算了给定时期内,股价的平均日内波动幅度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyIndicator(bt.Strategy):

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.datahigh = self.datas[0].high
self.datalow = self.datas[0].low

def next(self):
day_range_total = 0
for i in range(-13, 1):
day_range = self.datahigh[i] - self.datalow[i]
day_range_total += day_range
M_Indicator = day_range_total / 14

self.log('Close: %.2f, M_Indicator: %.4f' % (self.dataclose[0], M_Indicator))

下图是程序运行时输出的技术指标值

14.在Backtrader中绘图

尽管回测是一种基于数学计算的自动化过程,但人们往往希望通过可视化来了解到底发生了什么。有时人们可能对回测过程中的具体算法或者技术指标到底传达了什么信息感兴趣。

通过可视化绘制数据表,指标,操作,现金和投资组合价值的变化情况可以帮助人们:

  1. 更好地了解正在发生的事情;

  2. 否定/修改/创建想法;

  3. 以及人们在看到可视化结果后可能会做出的任何其他决策。

对单只股票数据进行可视化

在Backtrader中绘图非常简单,只需要在cerebro.run()之后接上一句cerebro.plot()。

这是一个图表示例,其中包含在示例中一直使用的TSLA数据。

该图由上中下三个区域构成,分别对应三个观察者CashValue Observer, Trade Observer和BuySell Observer:

  • CashValue Observer
    在回测运行期间跟踪现金和投资组合总价值(包括现金)的变化情况。

  • Observer Observer
    在交易结束时显示实际的损益,交易被定义为开仓及平仓这一对完整动作。

  • BuySell Observer
    在价格上方绘制买卖操作点

这3个观察者由cerebro自动添加,并由stdstats参数控制(默认值:True)。 如果需要,可通过以下命令禁用它们:

1
cerebro = bt.Cerebro(stdstats=False)

或者

1
2
3
cerebro = bt.Cerebro()
......
cerebro.run(stdstats=False)

对多只股票数据进行可视化

Backtrader可以同时将多个股票轻松地显示在一张图表上。如果需要可视化两个资产之间的相关性,这将很有用。

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
import datetime
import backtrader as bt

#初始化Cerebro引擎
cerebro = bt.Cerebro(stdstats=False)

#设置数据参数并添加至Cerebro引擎
data1 = bt.feeds.YahooFinanceCSVData(
dataname='TSLA.csv',
fromdate=datetime.datetime(2018, 1, 1),
todate=datetime.datetime(2020, 1, 1))
cerebro.adddata(data1)

data2 = bt.feeds.YahooFinanceCSVData(
dataname='AAPL.csv',
fromdate=datetime.datetime(2018, 1, 1),
todate=datetime.datetime(2020, 1, 1))

data2.compensate(data1)
data2.plotinfo.plotmaster = data1
data2.plotinfo.sameaxis = True
cerebro.adddata(data2)

#运行Cerebro引擎
cerebro.run()
cerebro.plot()

运行结果如下图所示:

将技术指标添加到图中

下面的代码给出如何给TSLA添加均线图。

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
import datetime
import backtrader as bt

#简单移动平均
class SimpleMA(bt.Strategy):
def __init__(self):
self.sma = bt.indicators.SimpleMovingAverage(self.data, period=20, plotname="20 SMA")

# 初始化Cerebro引擎, 禁用数据监测
cerebro = bt.Cerebro(stdstats=False)

# 设置日期参数并添加至Cerebro引擎
data1 = bt.feeds.YahooFinanceCSVData(
dataname='TSLA.csv',
fromdate=datetime.datetime(2018, 1, 1),
todate=datetime.datetime(2020, 10, 5))

cerebro.adddata(data1)

# 在图标上添加简单移动均线
cerebro.addstrategy(SimpleMA)

# 运行Cerebro引擎
cerebro.run()
cerebro.plot()

通过plotname可以指定技术指标对应的图例名,运行结果如下图所示:

15.使用另类数据

这里尝试根据Google搜索数据来评估情绪,并根据搜索量的任何明显变化进行交易。

首先从Google趋势下载比特币每周历史搜索数据并从Yahoo Finance获得价格数据。

由于2017年末波动很大,因此从2018年开始测试该策略。此后,搜索结果数据和价格均稳定了很长时间。Google趋势数据与Yahoo Finance数据采用的OHLC格式不同。因此,使用Backtrader提供的通用CSV模板添加数据。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data2 = bt.feeds.GenericCSVData(
dataname='BTC_Gtrends.csv',
fromdate=datetime.datetime(2018, 1, 1),
todate=datetime.datetime(2020, 1, 1),
nullvalue=0.0,
dtformat=('%Y-%m-%d'),
datetime=0,
time=-1,
high=-1,
low=-1,
open=-1,
close=1,
volume=-1,
openinterest=-1,
timeframe=bt.TimeFrame.Weeks)
cerebro.adddata(data2)

对于非OHLC数据,必须定义哪些列存在,哪些不存在。对数据中不存在的列分配值-1,并为可用的列分配递增的整数值。除此之外,其他代码和之前没有很大的变化。这是现在的Strategy类:

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
64
65
66
class BtcSentiment(bt.Strategy):
params = (('period', 10), ('devfactor', 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):
self.btc_price = self.datas[0].close
self.google_sentiment = self.datas[1].close
self.bbands = bt.indicators.BollingerBands(self.google_sentiment, period=self.params.period, devfactor=self.params.devfactor)

self.order = 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, %.2f' % order.executed.price)
elif order.issell():
self.log('SELL EXECUTED, %.2f' % order.executed.price)
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 next(self):
# 检查是否有正在执行的订单
if self.order:
return

# 看多信号
if self.google_sentiment > self.bbands.lines.top[0]:
# 检查是否有持仓
if not self.position:
self.log('Google Sentiment Value: %.2f' % self.google_sentiment[0])
self.log('Top band: %.2f' % self.bbands.lines.top[0])
# 没有持仓则开仓多头
self.log('***BUY CREATE, %.2f' % self.btc_price[0])
# 追踪订单避免重复开仓
self.order = self.buy()

# 看空信号
elif self.google_sentiment < self.bbands.lines.bot[0]:
# 检查是否有持仓
if not self.position:
self.log('Google Sentiment Value: %.2f' % self.google_sentiment[0])
self.log('Bottom band: %.2f' % self.bbands.lines.bot[0])
# 没有持仓则开仓空头
self.log('***SELL CREATE, %.2f' % self.btc_price[0])
# 追踪订单避免重复开仓
self.order = self.sell()

# 中性信号,对既有仓位平仓
else:
if self.position:
# 如果有仓位,则平仓
self.log('Google Sentiment Value: %.2f' % self.google_sentiment[0])
self.log('Bottom band: %.2f' % self.bbands.lines.bot[0])
self.log('Top band: %.2f' % self.bbands.lines.top[0])
self.log('CLOSE CREATE, %.2f' % self.btc_price[0])
self.order = self.close()

这里再次使用布林带策略,当搜索数量超过10日布林带上轨时开仓多头,少于10日布林带下轨时开仓空头。当搜索量介于上下轨之间时,若存在仓位则平仓,否则不采取任何行动。

运行回测后的结果如下:

16.其他补充的

Backtrader包含了许多功能,能够满足一般用户的绝大多数需求。
Backtrader有潜质成为商业解决方案,十分感激原作者将其保持开源。

在阅读完这两篇连载后,相信大家可在Backtrader中进行策略初探了。但在回测时,还需要注意一下几点:

  • 佣金
    交易费和佣金加起来,这些不容忽视。 Backtrader可以考虑固定金额和固定百分比两种情形的佣金。

  • 风险管理
    本教程主要突出了Backtrader的潜力,并为使用该平台的用户提供一个简明的教程,并没有怎么涉及风险管理。

依据不同类型的风险管理,实际的回测结果可能会有很大不同。量化的目标是在可接受的风险水平下优化策略获取最大的收益,而不是尝试以承担巨大风险为代价来最大化利润

最后,策略开发的重点应该是找到一个良好的基础策略,然后采用优化进行微小的调整。有时量化研究者陷入了完全相反的怪圈,即选择一个不太好的策略,试图通过数值优化使结果变得好看,这是难以盈利的

17.源码下载

百度网盘搜索【Python回测神器Backtrader源码】或 gitee下载地址