在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[