在上一篇的基础上,本篇主要讲讲如何在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 ): 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)) 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 datetimeimport backtrader as btfrom strategies import *cerebro = bt.Cerebro() data = bt.feeds.YahooFinanceCSVData( dataname='TSLA.csv' , fromdate=datetime.datetime(2016 , 1 , 1 ), todate=datetime.datetime(2017 , 12 , 25 )) cerebro.adddata(data) cerebro.addstrategy(MAcrossover) cerebro.addsizer(bt.sizers.SizerFix, stake=3 ) if __name__ == '__main__' : 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 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: 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() 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 : pass elif self.crossover < 0 : 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 datetimeimport backtrader as btfrom 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 )) cerebro.adddata(data) 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)
代码的主要变化如下:
当初始化Cerebro引擎时,将optreturn参数设置为False,即只要求输出策略的参数以及analyzer对策略运行结果的统计,来提高运行速度。
添加了一个analyzer类的对象分析不同参数组合策略的夏普比率。
移除了cerebro.addstrategy(),取而代之的是cerebro.optstrategy(),表明要对该策略进行优化,并限制了待优化参数的取值范围。
最终,优化结果存储在多个列表构成的列表对象optimized_runs中。遍历该列表并将快慢线的周期数据及相应夏普比率汇总并排序,得到最终结果为
当快线周期取7,慢线周期取92时,结果最优。现在开始对样本外数据进行分析,此时需要修改数据的起止日期。
1 2 fromdate=datetime.datetime(2018 , 1 , 1 ), todate=datetime.datetime(2019 , 12 , 25 ))
对于样本外数据,采用优化前和优化后的策略参数的盈亏分别为亏损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 datetimeimport backtrader as btfrom strategies import *cerebro = bt.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.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 ): 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中绘图
尽管回测是一种基于数学计算的自动化过程,但人们往往希望通过可视化来了解到底发生了什么。有时人们可能对回测过程中的具体算法或者技术指标到底传达了什么信息感兴趣。
通过可视化绘制数据表,指标,操作,现金和投资组合价值的变化情况可以帮助人们:
更好地了解正在发生的事情;
否定/修改/创建想法;
以及人们在看到可视化结果后可能会做出的任何其他决策。
对单只股票数据进行可视化 在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 datetimeimport backtrader as btcerebro = bt.Cerebro(stdstats=False ) 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.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 datetimeimport backtrader as btclass SimpleMA (bt.Strategy): def __init__ (self ): self.sma = bt.indicators.SimpleMovingAverage(self.data, period=20 , plotname="20 SMA" ) cerebro = bt.Cerebro(stdstats=False ) 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.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中进行策略初探了。但在回测时,还需要注意一下几点:
依据不同类型的风险管理,实际的回测结果可能会有很大不同。量化的目标是在可接受的风险水平下优化策略获取最大的收益,而不是尝试以承担巨大风险为代价来最大化利润 。
最后,策略开发的重点应该是找到一个良好的基础策略,然后采用优化进行微小的调整。有时量化研究者陷入了完全相反的怪圈,即选择一个不太好的策略,试图通过数值优化使结果变得好看,这是难以盈利的 。
17.源码下载 百度网盘搜索【Python回测神器Backtrader源码】或 gitee下载地址