回測原理
非專業(yè)人士操作股票,常常是看電視或者網(wǎng)上介紹了某種方法霉晕,第二天就根據(jù)自己的想象去操作了纵朋。然而每一種策略都有它的適應(yīng)范圍以及成功概率膳汪,51:49的優(yōu)勢對決策也沒什么幫助,掌握工具越多访雪,贏面越大详瑞。
回溯測試(Back testing),即回測臣缀,是用歷史數(shù)據(jù)驗(yàn)證策略坝橡。它使用預(yù)先選擇的時(shí)間段和選定股票的歷史數(shù)據(jù)代入策略,模擬交易肝陪,從而評價(jià)該策略的好壞驳庭。
回測需要注意的是:
- 很多策略是通過統(tǒng)計(jì)方法對歷史數(shù)據(jù)總結(jié)得出的方法,再放入歷史數(shù)據(jù)回測效果必然好氯窍,這并不代表該策略對未來數(shù)據(jù)也有效饲常。
- 回測用于驗(yàn)證策略,也可用于調(diào)參狼讨,但它本身并不產(chǎn)生策略贝淤。
- 同一策略在不同時(shí)段、不同地域政供、不同周期結(jié)果不同播聪,盡可能多做回測朽基。
- 判斷回測效果時(shí),需要設(shè)定基線离陶,比如與保本理財(cái)比較稼虎,盈利少且有風(fēng)險(xiǎn)不如存銀行。
既然原理如此簡單招刨,只要確定買點(diǎn)霎俩、賣點(diǎn)、金額沉眶,自己寫程序打却,也能計(jì)算出贏利比例,為何使用回測框架呢谎倔?一般回測框架可同時(shí)支持多支股票操作柳击,根據(jù)策略生成交易,并記錄下每條交易記錄和當(dāng)前狀態(tài)片习,計(jì)入交易費(fèi)用捌肴,考慮大宗交易對價(jià)格的影響,無法買入和賣出的情況毯侦,還支持一些公式和評價(jià)指標(biāo)……包含很多細(xì)節(jié)哭靖,使用工具更加方便和規(guī)范。本篇將介紹一些流行的回測框架及用法侈离。
回測框架
Quant中文意思是股市分析員试幽,也被譯為設(shè)計(jì)實(shí)現(xiàn)金融模型,很多量化交易平臺(tái)名字中都有這個(gè)單詞或者它的縮寫卦碾,比如極寬铺坞、聚寬、優(yōu)礦洲胖、米筐……
國內(nèi)的回測框架大都在線上使用济榨,比如優(yōu)礦,也是Python環(huán)境绿映,并且支持一些機(jī)器學(xué)習(xí)和深度學(xué)習(xí)工具擒滑。很多專業(yè)人士都不會(huì)把自己的策略上傳到云端,云端往往也不能做深入的數(shù)據(jù)處理叉弦。線上平臺(tái)后面文章再詳細(xì)討論丐一,本篇主要介紹本地運(yùn)行的回測框架。
使用線下工具需要下載數(shù)據(jù)淹冰,搭建環(huán)境库车,熟悉金融方面的三方庫,在前兩篇已介紹過樱拴。
Zipline
Quantopian是一個(gè)在線構(gòu)建量化交易策略的平臺(tái)柠衍,zipline是Quantopian開源的Pthon量化交易庫洋满,提供了Quantopian大部分的功能(如回測、研究)珍坊,據(jù)說優(yōu)礦牺勾、聚礦也是基于zipline框架。
Zipline主要用于美股垫蛆,國內(nèi)有人通過修改其源碼禽最,使之支持A股。
Pyalgotrade
Pyalgotrade簡稱PAT袱饭,也是離線的量化交易平臺(tái),它包含回測呛占、計(jì)算常用的技術(shù)指標(biāo)虑乖,可通過實(shí)現(xiàn)自己的數(shù)據(jù)類引入數(shù)據(jù)。用法簡單晾虑。缺點(diǎn)是PyAlgoTrade不支持Pandas的數(shù)據(jù)結(jié)構(gòu)疹味,因此需要做一些額外的數(shù)據(jù)轉(zhuǎn)換。后面重點(diǎn)介紹PAT工具的用法帜篇。
Zwquant
Zwquant極寬是一個(gè)簡單的量化交易平臺(tái)糙捺,作者團(tuán)隊(duì)寫了一套與之相應(yīng)的書籍,在當(dāng)當(dāng)上賣得不錯(cuò)笙隙。
軟件功能相對比較簡單洪灯,其核心代碼不過一兩千行,它的優(yōu)點(diǎn)是注釋和輸出都使用中文竟痰,對不熟悉金融領(lǐng)域?qū)I(yè)英語的用戶非常友好签钩,尤其是基于Pandas實(shí)現(xiàn)的一些金融函數(shù)都有詳細(xì)的中文注釋(函數(shù)實(shí)現(xiàn)主要借鑒panda_talib)。
缺點(diǎn)是它只能在Windows系統(tǒng)上運(yùn)行坏快,數(shù)據(jù)基于tushare(tushare舊接口目前只能提供兩年半數(shù)據(jù))铅檩,且歷史數(shù)據(jù)下載最近又被百度網(wǎng)盤封掉了……不過讀者還是可以從中學(xué)習(xí)回溯的原理、數(shù)據(jù)組織莽鸿、以及做圖的方法昧旨。
Pyalgotrade工具
安裝軟件
工具安裝
$ sudo pip install pyalgotrade
下載源碼
$ git clone git://www.github.com/gbeced/pyalgotrade.git
主要參考samples下例程
構(gòu)成
下面列出了pyalgotrade常用的幾個(gè)子模塊,其中又以數(shù)據(jù)采集和策略最為重要祥得,程序可繁可簡兔沃,最簡單的代碼二三十行即可實(shí)現(xiàn)。
- 數(shù)據(jù)采集pyalgotrade.barfeed 提供了一些常用的數(shù)據(jù)采集類啃沪,開發(fā)者也可基于采集基類自定義采集類粘拾。
- 策略pyalgotrade.strategy 繼承策略基類,開發(fā)者在其中實(shí)現(xiàn)具體策略:編寫邏輯创千,確定買入缰雇、賣出時(shí)間入偷,金額等等。
- 分析pyalgotrade.stratanalyzer 評價(jià)策略的運(yùn)行結(jié)果械哟,如:盈利/虧損金額疏之、次數(shù)、單位回報(bào)率等等暇咆。
- 技術(shù)指標(biāo)pyalgotrade.technical 常用的技術(shù)指標(biāo)锋爪,無需安裝其它軟件即可使用。
- 繪圖pyalgotrade. plotter 繪圖工具爸业,主要用于直觀地分析和顯示策略的結(jié)果其骄。
- 經(jīng)紀(jì)商pyalgotrade.broker
設(shè)置交易費(fèi)用等細(xì)節(jié),用于執(zhí)行訂單扯旷。
相關(guān)概念
- 夏普比率Sharpe Ratio
夏普比率綜合考慮了收益和風(fēng)險(xiǎn)拯爽,公式如下:
其中E(Rp)是預(yù)期報(bào)酬率,Rf是無風(fēng)險(xiǎn)利率钧忽,op是標(biāo)準(zhǔn)差毯炮,等號(hào)上面是收益,等號(hào)下面是風(fēng)險(xiǎn)耸黑,因此桃煎,該值越大越好。當(dāng)在幾種策略之間選擇時(shí)大刊,也可以考慮夏普比率为迈。
- 最大回撤率
在指定周期內(nèi),產(chǎn)品凈值走到最低點(diǎn)時(shí)的收益率回撤幅度的最大值奈揍,即:最壞情況下的虧損比例曲尸。 - 成交量加權(quán)平均價(jià)策略VWAP
對于較大的交易,如果全部按當(dāng)前市價(jià)下單男翰,會(huì)對市場造成巨大的沖擊另患,更好的方法是小批量分時(shí)下單。VWAP的目標(biāo)是最小化沖擊成本蛾绎,使交易價(jià)格等于一段時(shí)間內(nèi)的平均價(jià)格昆箕。在機(jī)構(gòu)和莊家大資金進(jìn)貨、出貨操作時(shí)需要考慮沖擊問題租冠,一般散戶很少使用鹏倘。 - Bar
在一定時(shí)間段內(nèi)的時(shí)間序列構(gòu)成了一根 K 線(蠟燭圖),單根K線被稱為 Bar顽爹。
評價(jià)指標(biāo)
評價(jià)函數(shù)在pyalgotrade.stratanalyzer子模塊中纤泵,下面列出了幾個(gè)常用的評價(jià)指標(biāo)類:
- pyalgotrade.stratanalyzer.returns.Returns() 收益率
- pyalgotrade.stratanalyzer.sharpe.SharpeRatio() 夏普比率
- pyalgotrade.stratanalyzer.drawdown.DrawDown() 回撤率
- pyalgotrade.stratanalyzer.trades.Trades() 具體交易 trade提供的信息最多,一般關(guān)注
getCount():總的交易次數(shù)
getProfitableCount():盈利的交易次數(shù)
getUnprofitableCount():虧損的交易次數(shù)
getEvenCount():不賺不虧的交易次數(shù)
getAll():返回numpy.array的數(shù)據(jù)镜粤,內(nèi)容是每次交易的盈虧
getProfits():返回numpy.array的數(shù)據(jù)捏题,內(nèi)容是每次盈利交易的盈利
getLosses():返回numpy.array的數(shù)據(jù)玻褪,內(nèi)容是每次虧損交易的虧損額
getAllReturns():返回numpy.array的數(shù)據(jù),內(nèi)容是每次交易的盈利(百分比)
getPositiveReturns():返回numpy.array的數(shù)據(jù)公荧,內(nèi)容是每次盈利交易的收益
getNegativeReturns():返回numpy.array的數(shù)據(jù)带射,內(nèi)容是每次虧損交易的損失
實(shí)例
本例中使用的是SMA移動(dòng)平均線策略,程序分成四部分循狰,第一部分引入三方庫窟社,第二部分實(shí)現(xiàn)數(shù)據(jù)采集類Feed,第三部分實(shí)現(xiàn)策略類MyStrategy绪钥,第四部分是主控和評價(jià)灿里。
from pyalgotrade import strategy # 策略
from pyalgotrade import plotter # 做圖
from pyalgotrade.technical import ma # 技術(shù)方法
from pyalgotrade.technical import cross # 技術(shù)方法
from pyalgotrade.stratanalyzer import returns # 評價(jià)
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): # 做自己的數(shù)據(jù)源,從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_) # 從數(shù)據(jù)流中組裝數(shù)據(jù)
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())
# 這個(gè)函數(shù)每天調(diào)一次
def onBars(self, bars):
bar = bars[self.__instrument] # bar是k線中的每個(gè)柱
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() # 評價(jià)
myStrategy.attachAnalyzer(retAnalyzer)
sharpeRatioAnalyzer = sharpe.SharpeRatio()
myStrategy.attachAnalyzer(sharpeRatioAnalyzer)
drawDownAnalyzer = drawdown.DrawDown()
myStrategy.attachAnalyzer(drawDownAnalyzer)
tradesAnalyzer = trades.Trades()
myStrategy.attachAnalyzer(tradesAnalyzer)
myStrategy.run() # 開始運(yùn)行程腹,然后事件驅(qū)動(dòng)
myStrategy.info("最終投資組合價(jià)值: $%.2f" % myStrategy.getResult())
print("最終資產(chǎn)價(jià)值: $%.2f" % myStrategy.getResult())
print("累計(jì)回報(bào)率: %.2f %%" % (retAnalyzer.getCumulativeReturns()[-1] * 100))
print("夏普比率: %.2f" % (sharpeRatioAnalyzer.getSharpeRatio(0.05)))
print("最大回撤率: %.2f %%" % (drawDownAnalyzer.getMaxDrawDown() * 100))
print("最長回撤時(shí)間: %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()