基於逐筆成交的高頻回測系統兼論K線回測的缺陷


在FMZ文庫發表了一篇文章,是我最近兩個月研究的總結,部分解決了當前所有回測系統的一個通病,並且可用於高頻交易回測和套利回測,值得參考,原文地址:https://www.fmz.com/digest-topic/5719

 

我在幣安做空超漲做多超跌多幣種對沖策略時,同時發布了一個回測引擎。並第一篇報告基於一小時K線回測,驗證了策略的有效性。但實際公開策略的休眠時間時1s,是一個相當高頻的策略,用小時K線回測顯然無法得出精確結果。后來補充了分鍾線回測的結果,回測收益提高了很多,但還是無法確定秒級情況下應該用什么參數,對整個策略的理解也不是很清晰。主要原因是基於K線的回測的重要弊端。

基於K線回測的問題

首先什么是歷史K線?一根K線數據包含高開低收四個價格、起始兩個時間以及區間成交量。大部分量化平台和框架都是基於K線回測的,FMZ量化平台也提供了tick級回測。K線回測的速度很快,大部分情況下也沒問題,但是也有非常嚴重的缺陷,特別是回測多品種策略和高頻策略,幾乎無法得出正確的結論。

首先是時間問題,K線數據最高價和最低價的時間是沒有給出的,不用考慮,但最重要的開盤和收盤價起始並不是開盤和收盤時間。即使不太冷門的交易品種,也往往十幾秒都沒有交易,而我們回測多品種策略時,往往默認它們的開盤價和收盤價是同時的,這也是基於收盤價回測的基礎。

想象一下用分鍾線回測兩個品種的套利,它們的差價通常10元,現在發現10:01時刻,A合約收盤價為100,B合約為112,差價是12元,於是策略開始對沖,某個時刻差價回歸,策略賺了2元的回歸利潤。

而實際情況可能是在10:00:45,A合約產生了一筆100元的成交,此后沒有交易,B合約在10:00:58發生了一筆112元的成交,在10:01這一刻,兩個價格都是不存在的,此時的盤口價格是多少呢,對沖能夠吃到多少差價呢?都無法知道。一個可能的情況是:在10:00:58時,A合約的買一賣一盤口是101.9-102.1,根本沒有2元差價。這就會對我們的策略優化產生很大的誤導。

其次是撮合問題,真實的撮合是價格優先,時間優先。如果買家超過賣一價,一般會直接以賣一價成交,反之進入訂單簿等待。K線數據顯然沒有買一賣一價,是無法模擬細節層次的撮合。

最后是策略本身成交對市場的影響,如果是小資金回測,影響不大。但如果成交量占比很大,會對市場產生沖擊。不僅是立即成交時價格滑點會很大,如果回測你的買單成交了,實際上搶占了其它原來要買入交易者的成交,蝴蝶效應下會對市場產生影響。而這種影響無法量化給出,只能憑借經驗說高頻交易只能容納小資金。

基於實時深度和tick的回測

FMZ提供了實盤級回測,能夠獲取到真實的歷史20檔深度,實時的秒級tick,逐筆成交等數據,並基於此做了實盤回放功能。這樣的回測數據量極大,回測速度也很慢,一般只能回測兩天。對於相對高頻或者對時間判斷要求嚴格的策略,實盤級回測是必須的。FMZ收集的交易對和時間並不長,但也有700多億條歷史數據。目前的撮合機制是如果買單大於賣一會不看量立即完全撮合,小於賣一進入撮合隊列排隊。這樣的回測機制解決了K線回測的前兩個問題,但還是無法解決最后一個問題。並且由於數據量實在太大,回測速度和時間范圍都有限制。

基於逐筆成交訂單流的回測機制

K線信息太少,深度也有可能是假深度,但有一種數據是市場真實的成交意願,反映了最真實的交易歷史———那就是逐筆成交。本文將提出一個基於訂單流的高頻回測系統,將大大減少實盤級回測的數據量,並且一定程度的模擬成交量對市場的影響。

我下載了最近5天幣安XTZ永續合約的逐筆成交(下載地址:https://www.fmz.com/upload/asset/1ff487b007e1a848ead.csv ),作為一個不算熱門的品種,共有213000條數據,先看一下數據的構成:

[['XTZ', 1590981301905, 2.905, 0.4, 'False\n'], ['XTZ', 1590981303044, 2.903, 3.6, 'True\n'], ['XTZ', 1590981303309, 2.903, 3.7, 'True\n'], ['XTZ', 1590981303738, 2.903, 238.1, 'True\n'], ['XTZ', 1590981303892, 2.904, 0.1, 'False\n'], ['XTZ', 1590981305250, 2.904, 0.1, 'False\n'], ['XTZ', 1590981305643, 2.903, 197.3, 'True\n'], 

數據是二維列表,按成交時間順序排序。具體意義分別是:品種名稱、成交價格、成交時間戳、成交數量、是否是賣單主動成交。有買有賣,每一筆成交都包含了買方和賣方,如果買方是做市maker,賣方是主動成交taker,則最后一個數據是True。

首先根據成交方向,可以相當精確的推測出市場上的買一和賣一,如果是主動賣出單,則此時的買一價就是成交價,如果主動買入單,則賣一價為成交價,有新的成交就更新新的盤口,未更新的保留上一次結果。很容易的推出以上數據的最后時刻,買一價為2.903,賣一價為2.904。

根據訂單流,可以這樣撮合:以一筆買單為例,價格為price,下單量為amount,此時盤口買一賣一分別為bid,ask。如果price低於ask高於bid,則先判斷為maker,並且可以優先撮合成交,則此后在訂單存在時間內所有的成交價低於或等於price的逐筆成交都與此訂單撮合(如果price低於或等於bid,則不能優先成交,成交價低於price的訂單都與此訂單撮合),撮合價為price,交易量為逐筆成交的成交量,直到訂單完全成交或者撤單。如果價格高於ask,判斷為taker,此后在訂單存在時間內所有的成交價低於或等於price的逐筆成交都與此訂單撮合,撮合價為逐筆成交的成交價。區分maker和taker是因為基本上交易所鼓勵掛單,有手續費的優惠,對於高頻策略,必須考慮這種區別。

可以很容易看到這種撮合的一個問題,如果訂單為taker,實際情況是能立即成交,而不是等待新的訂單與之撮合。首先我們並沒有考慮盤口掛單量,就算有數據,直接判斷成交也改變了深度,影響了市場。而基於新訂單的撮合,相當於把歷史中真實存在的訂單替換成你的訂單,無論如何也不會超出市場本身成交量的限制,最終盈利也不可能超過行情產生的最大盈利。部分的撮合機制也影響了訂單的成交量,進而影響策略的收益,定量的反映出了策略容量。不會出現傳統回測,資金量放大一倍收益就放大一倍的情況。

還有一些小細節,如果訂單買價等於買一,實際上仍然有一定的概率以買一價被撮合的,需要考慮掛單的優先級和成交概率等,較為復雜,這里就不考慮了。

撮合代碼

交易所對象可以參考開頭的介紹,基本不變,只添加了maker和taker手續費的區別,以及優化了回測的速度。下面將主要介紹撮合代碼。

    symbol = 'XTZ' loop_time = 0 intervel = 1000 #策略的休眠時間為1000ms init_price = data[0][2] #初始價格 e = Exchange([symbol],initial_balance=1000000,maker_fee=maker_fee,taker_fee=taker_fee,log='') #初始化交易所 depth = {'ask':data[0][2], 'bid':data[0][2]} #深度 order = {'buy':{'price':0,'amount':0,'maker':False,'priority':False,'id':0}, 'sell':{'price':0,'amount':0,'maker':False,'priority':False,'id':0}} #訂單 for tick in data: price = int(tick[2]/tick_sizes[symbol])*tick_sizes[symbol] #成交價格 trade_amount = tick[3] #成交數量 time_stamp = tick[1] #成交時間戳 if tick[4] == 'False\n': depth['ask'] = price else: depth['bid'] = price if depth['bid'] < order['buy']['price']: order['buy']['priority'] = True if depth['ask'] > order['sell']['price']: order['sell']['priority'] = True if price > order['buy']['price']: order['buy']['maker'] = True if price < order['sell']['price']: order['sell']['maker'] = True #訂單網絡延時也可以作為撮合條件之一,這里沒考慮 cond1 = order['buy']['priority'] and order['buy']['price'] >= price and order['buy']['amount'] > 0 cond2 = not order['buy']['priority'] and order['buy']['price'] > price and order['buy']['amount'] > 0 cond3 = order['sell']['priority'] and order['sell']['price'] <= price and order['sell']['amount'] > 0 cond4 = not order['sell']['priority'] and order['sell']['price'] < price and order['sell']['amount'] > 0 if cond1 or cond2: buy_price = order['buy']['price'] if order['buy']['maker'] else price e.Buy(symbol, buy_price, min(order['buy']['amount'],trade_amount), order['buy']['id'], order['buy']['maker']) order['buy']['amount'] -= min(order['buy']['amount'],trade_amount) e.Update(time_stamp,[symbol],{symbol:price}) if cond3 or cond4: sell_price = order['sell']['price'] if order['sell']['maker'] else price e.Sell(symbol, sell_price, min(order['sell']['amount'],trade_amount), order['sell']['id'], order['sell']['maker']) order['sell']['amount'] -= min(order['sell']['amount'],trade_amount) e.Update(time_stamp,[symbol],{symbol:price}) if time_stamp - loop_time > intervel: order = get_order(e,depth,order) #交易邏輯,這里未給出 loop_time += int((time_stamp - loop_time)/intervel)*intervel 

幾個細節要注意一下:

  • 1.當有新成交時,要先去撮合訂單,再去根據最新的價格去下單。
  • 2.每個訂單都有兩個屬性:maker——是否為maker,priority——撮合優先級,以買單為例,當買價小於賣一,標記為maker,當買價大於買一是標記為優先撮合,priority決定了價格等於買價是是否撮合,maker決定了手續費。
  • 3.訂單的maker和priority是更新的,如下了一筆很大的超過盤口的買單,當出現一個價格大於買價時,此時剩余的成交量將是maker。
  • 4.策略的intervel是必須的,它可以代表行情的延時。

網格策略的回測

終於到了實際的回測階段,我們這里回測一個最經典的網格策略,來看看有沒有達到預期的效果。策略原理為價格每上漲1%,我們就持有一定價值的空單(反之持有多單),計算好買單賣單提前掛好。代碼就不放出了。把所有代碼封裝到Grid('XTZ',100,0.3,1000,maker_fee=-0.00002,taker_fee=0.0003)函數中,參數分別為:交易對,價格偏離1%的持有價值,掛單密度0.3%,休眠間隔ms,掛單手續費,吃單手續費。

最近5天XTZ的行情處於震盪階段,很適合網格。 

我們先回測不同的持倉大小對收益的影響,傳統的回測機制回測出來的收益肯定會隨着持倉的增加等比增加。

e1 = Grid('XTZ',100,0.3,1000,maker_fee=-0.00002,taker_fee=0.0003) print(e1.account['USDT']) e2 = Grid('XTZ',1000,0.3,1000,maker_fee=-0.00002,taker_fee=0.0003) print(e2.account['USDT']) e3 = Grid('XTZ',10000,0.3,1000,maker_fee=-0.00002,taker_fee=0.0003) print(e3.account['USDT']) e4 = Grid('XTZ',100000,0.3,1000,maker_fee=-0.00002,taker_fee=0.0003) print(e4.account['USDT']) 

共回測了四組,持倉價值分別為100,1000,10000,100000,回測總用時1.3s。結果如下:

{'realised_profit': 28.470993031132966, 'margin': 0.7982662957624465, 'unrealised_profit': 0.0104554474048441, 'total': 10000028.481448, 'leverage': 0.0, 'fee': -0.3430967859046398, 'maker_fee': -0.36980249726699727, 'taker_fee': 0.026705711362357405} {'realised_profit': 275.63148945320177, 'margin': 14.346335829979132, 'unrealised_profit': 4.4382117331794045e-14, 'total': 10000275.631489, 'leverage': 0.0, 'fee': -3.3102045933457784, 'maker_fee': -3.5800688964477048, 'taker_fee': 0.2698643031019274} {'realised_profit': 2693.8701498889504, 'margin': 67.70120400534114, 'unrealised_profit': 0.5735269329348516, 'total': 10002694.443677, 'leverage': 0.0001, 'fee': -33.984021415250744, 'maker_fee': -34.879233866850974, 'taker_fee': 0.8952124516001403} {'realised_profit': 22610.231198585603, 'margin': 983.3853688758861, 'unrealised_profit': -20.529965947304365, 'total': 10022589.701233, 'leverage': 0.002, 'fee': -200.87094000385412, 'maker_fee': -261.5849078470078, 'taker_fee': 60.71396784315319} 

可以看到最終已實現利潤分別為持倉價值的28.4%,27.5%,26.9%,22.6%。這也符合實際情況,持倉的價值越大,掛單的價值越大,越可能出現部分成交的情況,最終實現的收益相對於掛單量也就越小。下圖是持倉價值分別為100和10000的相對收益對比:

我們還可以回測不同的參數對回測收益的影響,如掛單密度、休眠時間、手續費等。以休眠時間為例,改為100ms,對比休眠時間1000ms,看看收益情況。回測結果如下:

{'realised_profit': 29.079440803790423, 'margin': 0.7982662957624695, 'unrealised_profit': 0.0104554474048441, 'total': 10000029.089896, 'leverage': 0.0, 'fee': -0.3703702128662524, 'maker_fee': -0.37938946377435134, 'taker_fee': 0.009019250908098965} 

收益提高了一些,這是由於策略只掛了一組訂單,會有一些訂單由於來不及改變而吃不到波動的價格,休眠時間減少改善了這個問題。這也說明了網格策略掛多組訂單的重要性。

總結

本文創新的提出了一種新的基於訂單流的回測系統,可以部分模擬掛單、吃單、部分成交、延時等撮合情況,部分反映出了策略資金量對收益的影響,對於高頻策略和對沖策略有重要的參考價值,高精度的回測為策略參數優化指明了方向。也經過了長期實盤驗證。並且較好的控制了回測所需的數據量,回測速度也非常快。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM