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

回測原理

非專業(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()

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末钠四,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子跪楞,更是在濱河造成了極大的恐慌,老刑警劉巖侣灶,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甸祭,死亡現(xiàn)場離奇詭異,居然都是意外死亡褥影,警方通過查閱死者的電腦和手機(jī)池户,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來凡怎,“玉大人校焦,你說我怎么就攤上這事⊥车梗” “怎么了寨典?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長房匆。 經(jīng)常有香客問我耸成,道長,這世上最難降的妖魔是什么浴鸿? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任井氢,我火速辦了婚禮,結(jié)果婚禮上岳链,老公的妹妹穿的比我還像新娘花竞。我一直安慰自己,他們只是感情好掸哑,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布约急。 她就那樣靜靜地躺著零远,像睡著了一般。 火紅的嫁衣襯著肌膚如雪烤宙。 梳的紋絲不亂的頭發(fā)上遍烦,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音躺枕,去河邊找鬼服猪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛拐云,可吹牛的內(nèi)容都是我干的罢猪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼叉瘩,長吁一口氣:“原來是場噩夢啊……” “哼膳帕!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起薇缅,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤危彩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后泳桦,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體汤徽,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年灸撰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谒府。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡浮毯,死狀恐怖完疫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情债蓝,我是刑警寧澤壳鹤,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站惦蚊,受9級特大地震影響器虾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蹦锋,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一兆沙、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧莉掂,春花似錦葛圃、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽曲楚。三九已至,卻和暖如春褥符,著一層夾襖步出監(jiān)牢的瞬間龙誊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工喷楣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留趟大,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓铣焊,卻偏偏與公主長得像逊朽,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子曲伊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容