Python量化交易之三_回测框架

回测原理

非专业人士操作股票,常常是看电视或者网上介绍了某种方法,第二天就根据自己的想象去操作了。然而每一种策略都有它的适应范围以及成功概率,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()

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容