回测原理
非专业人士操作股票,常常是看电视或者网上介绍了某种方法,第二天就根据自己的想象去操作了。然而每一种策略都有它的适应范围以及成功概率,51:49的优势对决策也没什么帮助,掌握工具越多,赢面越大。
回溯测试(Back testing),即回测,是用历史数据验证策略。它使用预先选择的时间段和选定股票的历史数据代入策略,模拟交易,从而评价该策略的好坏。
回测需要注意的是:
- 很多策略是通过统计方法对历史数据总结得出的方法,再放入历史数据回测效果必然好,这并不代表该策略对未来数据也有效。
- 回测用于验证策略,也可用于调参,但它本身并不产生策略。
- 同一策略在不同时段、不同地域、不同周期结果不同,尽可能多做回测。
- 判断回测效果时,需要设定基线,比如与保本理财比较,盈利少且有风险不如存银行。
既然原理如此简单,只要确定买点、卖点、金额,自己写程序,也能计算出赢利比例,为何使用回测框架呢?一般回测框架可同时支持多支股票操作,根据策略生成交易,并记录下每条交易记录和当前状态,计入交易费用,考虑大宗交易对价格的影响,无法买入和卖出的情况,还支持一些公式和评价指标……包含很多细节,使用工具更加方便和规范。本篇将介绍一些流行的回测框架及用法。
回测框架
Quant中文意思是股市分析员,也被译为设计实现金融模型,很多量化交易平台名字中都有这个单词或者它的缩写,比如极宽、聚宽、优矿、米筐……
国内的回测框架大都在线上使用,比如优矿,也是Python环境,并且支持一些机器学习和深度学习工具。很多专业人士都不会把自己的策略上传到云端,云端往往也不能做深入的数据处理。线上平台后面文章再详细讨论,本篇主要介绍本地运行的回测框架。
使用线下工具需要下载数据,搭建环境,熟悉金融方面的三方库,在前两篇已介绍过。
Zipline
Quantopian是一个在线构建量化交易策略的平台,zipline是Quantopian开源的Pthon量化交易库,提供了Quantopian大部分的功能(如回测、研究),据说优矿、聚矿也是基于zipline框架。
Zipline主要用于美股,国内有人通过修改其源码,使之支持A股。
Pyalgotrade
Pyalgotrade简称PAT,也是离线的量化交易平台,它包含回测、计算常用的技术指标,可通过实现自己的数据类引入数据。用法简单。缺点是PyAlgoTrade不支持Pandas的数据结构,因此需要做一些额外的数据转换。后面重点介绍PAT工具的用法。
Zwquant
Zwquant极宽是一个简单的量化交易平台,作者团队写了一套与之相应的书籍,在当当上卖得不错。
软件功能相对比较简单,其核心代码不过一两千行,它的优点是注释和输出都使用中文,对不熟悉金融领域专业英语的用户非常友好,尤其是基于Pandas实现的一些金融函数都有详细的中文注释(函数实现主要借鉴panda_talib)。
缺点是它只能在Windows系统上运行,数据基于tushare(tushare旧接口目前只能提供两年半数据),且历史数据下载最近又被百度网盘封掉了……不过读者还是可以从中学习回溯的原理、数据组织、以及做图的方法。
Pyalgotrade工具
安装软件
工具安装
$ sudo pip install pyalgotrade
下载源码
$ git clone git://www.github.com/gbeced/pyalgotrade.git
主要参考samples下例程
构成
下面列出了pyalgotrade常用的几个子模块,其中又以数据采集和策略最为重要,程序可繁可简,最简单的代码二三十行即可实现。
- 数据采集pyalgotrade.barfeed 提供了一些常用的数据采集类,开发者也可基于采集基类自定义采集类。
- 策略pyalgotrade.strategy 继承策略基类,开发者在其中实现具体策略:编写逻辑,确定买入、卖出时间,金额等等。
- 分析pyalgotrade.stratanalyzer 评价策略的运行结果,如:盈利/亏损金额、次数、单位回报率等等。
- 技术指标pyalgotrade.technical 常用的技术指标,无需安装其它软件即可使用。
- 绘图pyalgotrade. plotter 绘图工具,主要用于直观地分析和显示策略的结果。
- 经纪商pyalgotrade.broker
设置交易费用等细节,用于执行订单。
相关概念
- 夏普比率Sharpe Ratio
夏普比率综合考虑了收益和风险,公式如下:
其中E(Rp)是预期报酬率,Rf是无风险利率,op是标准差,等号上面是收益,等号下面是风险,因此,该值越大越好。当在几种策略之间选择时,也可以考虑夏普比率。
- 最大回撤率
在指定周期内,产品净值走到最低点时的收益率回撤幅度的最大值,即:最坏情况下的亏损比例。 - 成交量加权平均价策略VWAP
对于较大的交易,如果全部按当前市价下单,会对市场造成巨大的冲击,更好的方法是小批量分时下单。VWAP的目标是最小化冲击成本,使交易价格等于一段时间内的平均价格。在机构和庄家大资金进货、出货操作时需要考虑冲击问题,一般散户很少使用。 - Bar
在一定时间段内的时间序列构成了一根 K 线(蜡烛图),单根K线被称为 Bar。
评价指标
评价函数在pyalgotrade.stratanalyzer子模块中,下面列出了几个常用的评价指标类:
- pyalgotrade.stratanalyzer.returns.Returns() 收益率
- pyalgotrade.stratanalyzer.sharpe.SharpeRatio() 夏普比率
- pyalgotrade.stratanalyzer.drawdown.DrawDown() 回撤率
- pyalgotrade.stratanalyzer.trades.Trades() 具体交易 trade提供的信息最多,一般关注
getCount():总的交易次数
getProfitableCount():盈利的交易次数
getUnprofitableCount():亏损的交易次数
getEvenCount():不赚不亏的交易次数
getAll():返回numpy.array的数据,内容是每次交易的盈亏
getProfits():返回numpy.array的数据,内容是每次盈利交易的盈利
getLosses():返回numpy.array的数据,内容是每次亏损交易的亏损额
getAllReturns():返回numpy.array的数据,内容是每次交易的盈利(百分比)
getPositiveReturns():返回numpy.array的数据,内容是每次盈利交易的收益
getNegativeReturns():返回numpy.array的数据,内容是每次亏损交易的损失
实例
本例中使用的是SMA移动平均线策略,程序分成四部分,第一部分引入三方库,第二部分实现数据采集类Feed,第三部分实现策略类MyStrategy,第四部分是主控和评价。
from pyalgotrade import strategy # 策略
from pyalgotrade import plotter # 做图
from pyalgotrade.technical import ma # 技术方法
from pyalgotrade.technical import cross # 技术方法
from pyalgotrade.stratanalyzer import returns # 评价
from pyalgotrade.stratanalyzer import sharpe
from pyalgotrade.stratanalyzer import drawdown
from pyalgotrade.stratanalyzer import trades
from pyalgotrade.barfeed import membf
from pyalgotrade import bar
import tushare as ts
import pandas as pd
class Feed(membf.BarFeed): # 做自己的数据源,从tushare中读取
def __init__(self, frequency = bar.Frequency.DAY, maxLen=None):
super(Feed, self).__init__(frequency, maxLen)
def rowParser(self, ds, frequency=bar.Frequency.DAY):
dt = pd.to_datetime(ds["date"])
open = float(ds["open"])
close = float(ds["close"])
high = float(ds["high"])
low = float(ds["low"])
volume = float(ds["volume"])
return bar.BasicBar(dt, open, high, low, close, volume, None, frequency)
def barsHaveAdjClose(self):
return False
def addBarsFromCode(self, code, start, end, ktype="D", index=False):
frequency = bar.Frequency.DAY
ds = ts.get_k_data(code = code, start = start, end = end,
ktype = ktype, index = index)
bars_ = []
for i in ds.index:
bar_ = self.rowParser(ds.loc[i], frequency)
bars_.append(bar_)
self.addBarsFromSequence(code, bars_) # 从数据流中组装数据
class MyStrategy(strategy.BacktestingStrategy): # 继承策略的父类
def __init__(self, feed, instrument, smaPeriod):
super(MyStrategy, self).__init__(feed)
self.__instrument = instrument
self.__closed = feed[instrument].getCloseDataSeries()
self.__ma = ma.SMA(self.__closed, smaPeriod)
self.__position = None
def getSMA(self):
return self.__ma
def onEnterCanceled(self, position):
self.__position = None
print("onEnterCanceled", position.getShares())
def onExitOk(self, position):
self.__position = None
print("onExitOk", position.getShares())
def onExitCanceled(self, position):
self.__position.exitMarket()
print("onExitCanceled", position.getShares())
# 这个函数每天调一次
def onBars(self, bars):
bar = bars[self.__instrument] # bar是k线中的每个柱
if self.__position is None:
if cross.cross_above(self.__closed, self.__ma) > 0:
shares = int(self.getBroker().getCash() * 0.9 / bar.getPrice())
print("cross_above shares,", shares)
self.__position = self.enterLong(self.__instrument, shares, True)
elif not self.__position.exitActive() and cross.cross_below(self.__closed, self.__ma) > 0:
print("cross_below", bar.getPrice(), bar.getClose(), bar.getDateTime())
print(bars.keys())
print("length", len(self.__closed), self.__closed[-1])
self.__position.exitMarket()
def getClose(self):
return self.__closed
code = "002230"
feed = Feed()
feed.addBarsFromCode(code,start='2018-01-29',end='2019-09-04')
myStrategy = MyStrategy(feed, code, 20) # 最重要的策略类
plt = plotter.StrategyPlotter(myStrategy) # 做图分析
plt.getInstrumentSubplot(code).addDataSeries("SMA", myStrategy.getSMA())
retAnalyzer = returns.Returns() # 评价
myStrategy.attachAnalyzer(retAnalyzer)
sharpeRatioAnalyzer = sharpe.SharpeRatio()
myStrategy.attachAnalyzer(sharpeRatioAnalyzer)
drawDownAnalyzer = drawdown.DrawDown()
myStrategy.attachAnalyzer(drawDownAnalyzer)
tradesAnalyzer = trades.Trades()
myStrategy.attachAnalyzer(tradesAnalyzer)
myStrategy.run() # 开始运行,然后事件驱动
myStrategy.info("最终投资组合价值: $%.2f" % myStrategy.getResult())
print("最终资产价值: $%.2f" % myStrategy.getResult())
print("累计回报率: %.2f %%" % (retAnalyzer.getCumulativeReturns()[-1] * 100))
print("夏普比率: %.2f" % (sharpeRatioAnalyzer.getSharpeRatio(0.05)))
print("最大回撤率: %.2f %%" % (drawDownAnalyzer.getMaxDrawDown() * 100))
print("最长回撤时间: %s" % (drawDownAnalyzer.getLongestDrawDownDuration()))
print("")
print("总交易 Total trades: %d" % (tradesAnalyzer.getCount()))
if tradesAnalyzer.getCount() > 0:
profits = tradesAnalyzer.getAll()
print("利润", "mean", round(profits.mean(),2), "std", round(profits.std(),2),
"max", round(profits.max(),2), "min", round(profits.min(),2))
returns = tradesAnalyzer.getAllReturns()
print("收益率", "mean", round(returns.mean(),2), "std", round(returns.std(),2),
"max", round(returns.max(),2), "min", round(returns.min(),2))
print("")
print("赢利交易 Profitable trades: %d" % (tradesAnalyzer.getProfitableCount()))
if tradesAnalyzer.getProfitableCount() > 0:
profits = tradesAnalyzer.getProfits()
print("利润", "mean", round(profits.mean(),2), "std", round(profits.std(),2),
"max", round(profits.max(),2), "min", round(profits.min(),2))
returns = tradesAnalyzer.getPositiveReturns()
print("收益率", "mean", round(returns.mean(),2), "std", round(returns.std(),2),
"max", round(returns.max(),2), "min", round(returns.min(),2))
print("")
print("亏损交易Unprofitable trades: %d" % (tradesAnalyzer.getUnprofitableCount()))
if tradesAnalyzer.getUnprofitableCount() > 0:
losses = tradesAnalyzer.getLosses()
print("利润", "mean", round(losses.mean(),2), "std", round(losses.std(),2),
"max", round(losses.max(),2), "min", round(losses.min(),2))
returns = tradesAnalyzer.getNegativeReturns()
print("收益率", "mean", round(returns.mean(),2), "std", round(returns.std(),2),
"max", round(returns.max(),2), "min", round(returns.min(),2))
plt.plot()