WonderTrader高頻交易初探及v0.6發布 雁過也
3 人贊同了該文章 自從WonderTrader實現了HFT策略引擎以來,一直都沒有時間徹底的將高頻策略研發、回測、仿真、實盤整個流程徹底走通一遍。所以趁著最近公司要上高頻的機會,筆者基于WonderTrader把高頻策略的應用徹底梳理了一遍。
本文的主要目的就是幫助用戶初步了解WonderTrader的HFT引擎上如何開發策略的。
平臺準備 之前實盤框架下的HFT引擎已經基本完成,但是回測框架下的HFT策略的支持因為事情太多一直沒有完善。這次徹底梳理HFT引擎,正好把回測部分也完善了一下。HFT回測引擎完善之后,WonderTrader也正好發布一個新版本v0.6.0。
v0.6.0更新要點 CTA引擎設置目標倉位時,同時訂閱tick數據,主要針對標的不確定的策略,例如截面因子CTA策略 CTA回測引擎中,輸出的平倉明細中新增“最大潛在收益”和“最大潛在虧損”兩個字段 HFT引擎的回測進行了一次徹底的整理實現,基本滿足了HFT策略回測的需求(已測試) 初步完成了HFT引擎對股票Level2數據(orderqueue,orderdetail,transaction)的訪問接口 WtPorter和WtBtPorter兩個C接口粘合模塊,初步完成了C接口對股票Level2數據的支持
高頻模型介紹 本文采用的高頻模型,源自Darryl Shen(Linacre College University of Oxford)于2015年5月27日發表的《Order Imbalance Based Strategy in High Frequency Trading》一文(網絡上可以找到)。
該模型基于L筆tick數據中的委托量的不平衡因子、委比因子以及中間價回歸因子三個因子,預測t0時刻之后的k筆tick數據的中間價的均價變化量,并以此構建線性模型。通過線性回歸,得到各個因子的系數。線性方程如下:
方程中各個符號的具體含義,請感興趣的讀者自行檢索。該文中使用2014年IF主力合約全年的tick進行回測,每次進出場以1手股指為單位,可以實現92.6% 的勝率,最優參數下,年化夏普率可以達到7.243,日均P&L在58600元。
模型實現 有了模型以后,我們開始來編寫代碼實現。因為本文旨在介紹HFT策略開發的流程,為了降低讀者理解難度,策略都采用Python編寫。
策略框架介紹 首先我們來看一下一個高頻策略的基本結構:
class BaseHftStrategy:
'' 'HFT策略基礎類,所有的策略都從該類派生\n包含了策略的基本開發框架' '' def __init__
( self, name
) :self.__name__
= namedef name
( self
) :
return self.__name__def on_init
( self, context:HftContext
) :
'' '策略初始化,啟動的時候調用\n用于加載自定義數據\n@context 策略運行上下文' '' return def on_tick
( self, context:HftContext, stdCode:str, newTick:dict
) :
'' 'Tick數據進來時調用\n@context 策略運行上下文\n@stdCode 合約代碼\n@newTick 最新Tick' '' return def on_order_detail
( self, context:HftContext, stdCode:str, newOrdQue:dict
) :
'' '逐筆委托數據進來時調用\n@context 策略運行上下文\n@stdCode 合約代碼\n@newOrdQue 最新逐筆委托' '' return def on_order_queue
( self, context:HftContext, stdCode:str, newOrdQue:dict
) :
'' '委托隊列數據進來時調用\n@context 策略運行上下文\n@stdCode 合約代碼\n@newOrdQue 最新委托隊列' '' return def on_transaction
( self, context:HftContext, stdCode:str, newTrans:dict
) :
'' '逐筆成交數據進來時調用\n@context 策略運行上下文\n@stdCode 合約代碼\n@newTrans 最新逐筆成交' '' return def on_bar
( self, context:HftContext, stdCode:str, period:str, newBar:dict
) :
'' 'K線閉合時回調@context 策略上下文\n@stdCode 合約代碼@period K線周期@newBar 最新閉合的K線' '' return def on_channel_ready
( self, context:HftContext
) :
'' '交易通道就緒通知\n@context 策略上下文\n' '' return def on_channel_lost
( self, context:HftContext
) :
'' '交易通道丟失通知\n@context 策略上下文\n' '' return def on_entrust
( self, context:HftContext, localid:int, stdCode:str, bSucc:bool, msg:str, userTag:str
) :
'' '下單結果回報@context 策略上下文\n@localid 本地訂單id\n@stdCode 合約代碼\n@bSucc 下單結果\n@mes 下單結果描述' '' return def on_order
( self, context:HftContext, localid:int, stdCode:str, isBuy:bool, totalQty:float, leftQty:float, price:float, isCanceled:bool, userTag:str
) :
'' '訂單回報@context 策略上下文\n@localid 本地訂單id\n@stdCode 合約代碼\n@isBuy 是否買入\n@totalQty 下單數量\n@leftQty 剩余數量\n@price 下單價格\n@isCanceled 是否已撤單' '' return def on_trade
( self, context:HftContext, localid:int, stdCode:str, isBuy:bool, qty:float, price:float, userTag:str
) :
'' '成交回報@context 策略上下文\n@stdCode 合約代碼\n@isBuy 是否買入\n@qty 成交數量\n@price 成交價格' '' return
整個策略的結構大致可以分為四塊:
策略本身的回調 行情數據的回調 交易通道的回調 交易回報的回調 其中行情數據的回調,主要包括on_tick、on_bar和level2數據回調,本文中只需要關注on_tick即可;交易通道的回調,主要是通知策略交易通道的連接和斷開事件;交易回報的回調,主要是訂單回報、成交回報以及下單回報。
參數設計 根據模型的邏輯,我們設置回溯tick數為5,中間價變動的閾值為0.3,那么我們便可以將策略參數設計如下:
'' '交易參數' ''
self.__code__
= code
self.__expsecs__
= expsecs
self.__freq__
= freq self.__lots__
= lots self.count
= count
self.beta_0
= beta_0
self.beta_r
= beta_r
self.threshold
= threshold
self.beta_oi
= beta_oi
self.beta_rou
= beta_rou
self.active_secs
= active_secs
self.stoppl
= stoppl
核心邏輯
在大致了解了HFT策略的結構以后,我們就可以開始來編碼了。整個策略的核心邏輯,集中在on_tick回調中,主要就是上述模型的計算,代碼如下:hisTicks
= context.stra_get_ticks
( self.__code__, self.count + 1
)
if hisTicks.size
!= self.count+1:
return if ( len
( newTick
[ "askprice" ] ) == 0
) or
( len
( newTick
[ "bidprice" ] ) == 0
) :
return spread
= newTick
[ "askprice" ] [ 0
] - newTick
[ "bidprice" ] [ 0
] total_OIR
= 0.0
total_rou
= 0.0
for i
in range
( 1, self.count + 1
) :prevTick
= hisTicks.get_tick
( i-1
) curTick
= hisTicks.get_tick
( i
) lastBidPx
= self.get_price
( prevTick, -1
) lastAskPx
= self.get_price
( prevTick, 1
) lastBidQty
= prevTick
[ "bidqty" ] [ 0
] if len
( prevTick
[ "bidqty" ] ) > 0
else 0lastAskQty
= prevTick
[ "askqty" ] [ 0
] if len
( prevTick
[ "askqty" ] ) > 0
else 0curBidPx
= self.get_price
( curTick, -1
) curAskPx
= self.get_price
( curTick, 1
) curBidQty
= curTick
[ "bidqty" ] [ 0
] if len
( curTick
[ "bidqty" ] ) > 0
else 0curAskQty
= curTick
[ "askqty" ] [ 0
] if len
( curTick
[ "askqty" ] ) > 0
else 0delta_vb
= 0.0delta_va
= 0.0
if curBidPx
< lastBidPx:delta_vb
= 0.0
elif curBidPx
== lastBidPx:delta_vb
= curBidQty - lastBidQtyelse:delta_vb
= curBidQty
if curAskPx
< lastAskPx:delta_va
= curAskQty
elif curAskPx
== lastAskPx:delta_va
= curAskQty - lastAskQtyelse:delta_va
= 0.0voi
= delta_vb - delta_vatotal_OIR +
= self.beta_oi
[ i-1
] *voi/spreadrou
= ( curBidQty - curAskQty
) /
( curBidQty + curAskQty
) total_rou +
= self.beta_rou
[ i-1
] *rou/spreadprevTick
= hisTicks.get_tick
( -2
)
prevMP
= ( self.get_price
( prevTick, -1
) + self.get_price
( prevTick, 1
)) /2
curMP
= ( newTick
[ "askprice" ] [ 0
] + newTick
[ "bidprice" ] [ 0
] ) /2
if newTick
[ "volumn" ] != 0:avgTrdPx
= newTick
[ "turn_over" ] /newTick
[ "volumn" ] /self.__comm_info__.volscale
elif self._last_atp__
!= 0:avgTrdPx
= self._last_atp__
else:avgTrdPx
= curMPself._last_atp__
= avgTrdPx
curR
= avgTrdPx -
( prevMP + curMP
) / 2
efpc
= self.beta_0 + total_OIR + total_rou + self.beta_r * curR / spread
if efpc
>= self.threshold:targetPos
= self.__lots__diffPos
= targetPos - curPos
if diffPos
!= 0.0:targetPx
= newTick
[ "askprice" ] [ 0
] ids
= context.stra_buy
( self.__code__, targetPx, abs
( diffPos
) ,
"enterlong" ) for localid
in ids:self.__orders__
[ localid
] = localidself.__last_entry_time__
= nowself._max_dyn_prof
= 0self._max_dyn_loss
= 0
elif efpc
<= -self.threshold:targetPos
= -self.__lots__diffPos
= targetPos - curPos
if diffPos
!= 0:targetPx
= newTick
[ "bidprice" ] [ 0
] ids
= context.stra_sell
( self.__code__, targetPx, abs
( diffPos
) ,
"entershort" ) for localid
in ids:self.__orders__
[ localid
] = localidself.__last_entry_time__
= nowself._max_dyn_prof
= 0.0self._max_dyn_loss
= 0.0
止盈止損邏輯
但是對于高頻策略,除了核心的進出場邏輯之外,止盈止損邏輯也是非常重要的一部分。本文中的策略采用固定點位止損+跟蹤止盈來作為止盈止損邏輯,代碼如下:
if curPos
!= 0 and self.stoppl
[ "active" ] :isLong
= ( curPos
> 0
) price
= 0
if self.stoppl
[ "calc_price" ] == 0:price
= self.get_price
( newTick, -1
) if isLong
else self.get_price
( newTick, 1
) else:price
= newTick
[ "price" ] diffTicks
= ( price - self._last_entry_price
) *
( 1
if isLong
else -1
) / self.__comm_info__.pricetick
if diffTicks
> 0:self._max_dyn_prof
= max
( self._max_dyn_prof, diffTicks
) else:self._max_dyn_loss
= min
( self._max_dyn_loss, diffTicks
) bNeedExit
= Falseusertag
= '' stop_ticks
= self.stoppl
[ "stop_ticks" ] track_threshold
= self.stoppl
[ "track_threshold" ] fallback_boundary
= self.stoppl
[ "fallback_boundary" ] if diffTicks
<= stop_ticks:context.stra_log_text
( "浮虧%.0f超過%d跳,止損離場" %
( diffTicks, stop_ticks
)) bNeedExit
= Trueusertag
= "stoploss" elif self._max_dyn_prof
>= track_threshold and diffTicks
<= fallback_boundary:context.stra_log_text
( "浮贏回撤%.0f->%.0f[閾值%.0f->%.0f],止盈離場" %
( self._max_dyn_prof, diffTicks, track_threshold, fallback_boundary
)) bNeedExit
= Trueusertag
= "stopprof" if bNeedExit:targetprice
= self.get_price
( newTick, -1
) if isLong
else self.get_price
( newTick, 1
) ids
= context.stra_sell
( self.__code__, targetprice, abs
( curPos
) , usertag
) if isLong
else context.stra_buy
( self.__code__, price, abs
( curPos
) , usertag
) for localid
in ids:self.__orders__
[ localid
] = localid
return
收盤前出場的邏輯
有了止盈止損邏輯,我們還需要添加一段收盤前出場的邏輯,代碼如下:curMin
= context.stra_get_time
( )
curPos
= context.stra_get_position
( stdCode
)
if not self.is_active
( curMin
) :self._last_atp__
= 0.0
if curPos
== 0:
return self.__to_clear__
= True
else:self.__to_clear__
= False
if self.__to_clear__
: if self.__cancel_cnt__
== 0:
if curPos
> 0:targetPx
= self.get_price
( newTick, -1
) ids
= context.stra_sell
( self.__code__, targetPx, abs
( curPos
) ,
"deadline" ) for localid
in ids:self.__orders__
[ localid
] = localid
elif curPos
< 0:targetPx
= self.get_price
( newTick, 1
) ids
= context.stra_buy
( self.__code__, targetPx, abs
( curPos
) ,
"deadline" ) for localid
in ids:self.__orders__
[ localid
] = localid
return
訂單管理邏輯
然后,我們還需要添加一段訂單管理的邏輯,代碼如下:def check_orders
( self, ctx:HftContext
) :ord_cnt
= len
( self.__orders__.keys
( )) if ord_cnt
> 0 and self.__last_entry_time__ is not None:now
= makeTime
( ctx.stra_get_date
( ) , ctx.stra_get_time
( ) , ctx.stra_get_secs
( )) span
= now - self.__last_entry_time__total_secs
= span.total_seconds
( ) if total_secs
>= self.__expsecs__: ctx.stra_log_text
( "%d條訂單超時撤單" %
( ord_cnt
)) for localid
in self.__orders__:ctx.stra_cancel
( localid
) self.__cancel_cnt__ +
= 1ctx.stra_log_text
( "在途撤單數 -> %d" %
( self.__cancel_cnt__
))
其他邏輯
除了上述的邏輯之外,我們還需要處理一些細節問題,如:處理訂單回報,用于更新本地訂單的狀態;
處理成交回報,用于更新入場價格,計算浮動盈虧
處理交易通道就緒的回報,用于檢查是否有不在管理內的未完成單
完整源碼
整個策略的完整代碼如下:from wtpy
import BaseHftStrategy
from wtpy
import HftContextfrom datetime
import datetimedef makeTime
( date:int, time:int, secs:int
) :
'' '將系統時間轉成datetime\n@date 日期,格式如20200723\n@time 時間,精確到分,格式如0935\n@secs 秒數,精確到毫秒,格式如37500' '' return datetime
( year
= int
( date/10000
) , month
= int
( date%10000/100
) , day
= date%100, hour
= int
( time/100
) , minute
= time%100, second
= int
( secs/1000
) , microsecond
= secs%1000*1000
) class HftStraOrderImbalance
( BaseHftStrategy
) :def __init__
( self, name:str, code:str, count:int, lots:int, beta_0:float, beta_r:float, threshold:float, beta_oi:list, beta_rou:list, expsecs:int, offset:int, freq:int, active_secs:list, stoppl:dict, reserve:int
= 0
) :BaseHftStrategy.__init__
( self, name
) '' '交易參數' '' self.__code__
= code self.__expsecs__
= expsecs self.__freq__
= freq self.__lots__
= lots self.count
= count self.beta_0
= beta_0 self.beta_r
= beta_r self.threshold
= threshold self.beta_oi
= beta_oi self.beta_rou
= beta_rou self.active_secs
= active_secs self.stoppl
= stoppl
'' '內部數據' '' self.__last_tick__
= None self.__orders__
= dict
( ) self.__last_entry_time__
= None self.__cancel_cnt__
= 0 self.__channel_ready__
= False self.__comm_info__
= Noneself.__to_clear__
= Falseself._last_entry_price
= 0.0self._max_dyn_prof
= 0.0self._max_dyn_loss
= 0.0self._last_atp__
= 0.0def is_active
( self, curMin:int
) -
> bool:
for sec
in self.active_secs:
if sec
[ "start" ] <= curMin and curMin
<= sec
[ "end" ] :
return True
return Falsedef on_init
( self, context:HftContext
) :
'' '策略初始化,啟動的時候調用\n用于加載自定義數據\n@context 策略運行上下文' '' self.__comm_info__
= context.stra_get_comminfo
( self.__code__
) context.stra_sub_ticks
( self.__code__
) self.__ctx__
= contextdef check_orders
( self, ctx:HftContext
) :ord_cnt
= len
( self.__orders__.keys
( )) if ord_cnt
> 0 and self.__last_entry_time__ is not None:now
= makeTime
( ctx.stra_get_date
( ) , ctx.stra_get_time
( ) , ctx.stra_get_secs
( )) span
= now - self.__last_entry_time__total_secs
= span.total_seconds
( ) if total_secs
>= self.__expsecs__: ctx.stra_log_text
( "%d條訂單超時撤單" %
( ord_cnt
)) for localid
in self.__orders__:ctx.stra_cancel
( localid
) self.__cancel_cnt__ +
= 1ctx.stra_log_text
( "在途撤單數 -> %d" %
( self.__cancel_cnt__
)) def get_price
( self, newTick, pricemode
= 0
) :
if pricemode
== 0:
return newTick
[ "price" ] elif pricemode
== 1:
return newTick
[ "askprice" ] [ 0
] if len
( newTick
[ "askprice" ] ) > 0
else newTick
[ "price" ] elif pricemode
== -1:
return newTick
[ "bidprice" ] [ 0
] if len
( newTick
[ "bidprice" ] ) > 0
else newTick
[ "price" ] def on_tick
( self, context:HftContext, stdCode:str, newTick:dict
) :
if self.__code__
!= stdCode:
return if len
( self.__orders__.keys
( )) != 0:self.check_orders
( context
) return if not self.__channel_ready__:
return curMin
= context.stra_get_time
( ) curPos
= context.stra_get_position
( stdCode
) if not self.is_active
( curMin
) :self._last_atp__
= 0.0
if curPos
== 0:
return self.__to_clear__
= Trueelse:self.__to_clear__
= False
if self.__to_clear__
: if self.__cancel_cnt__
== 0:
if curPos
> 0:targetPx
= self.get_price
( newTick, -1
) ids
= context.stra_sell
( self.__code__, targetPx, abs
( curPos
) ,
"deadline" ) for localid
in ids:self.__orders__
[ localid
] = localid
elif curPos
< 0:targetPx
= self.get_price
( newTick, 1
) ids
= context.stra_buy
( self.__code__, targetPx, abs
( curPos
) ,
"deadline" ) for localid
in ids:self.__orders__
[ localid
] = localid
return if curPos
!= 0 and self.stoppl
[ "active" ] :isLong
= ( curPos
> 0
) price
= 0
if self.stoppl
[ "calc_price" ] == 0:price
= self.get_price
( newTick, -1
) if isLong
else self.get_price
( newTick, 1
) else:price
= newTick
[ "price" ] diffTicks
= ( price - self._last_entry_price
) *
( 1
if isLong
else -1
) / self.__comm_info__.pricetick
if diffTicks
> 0:self._max_dyn_prof
= max
( self._max_dyn_prof, diffTicks
) else:self._max_dyn_loss
= min
( self._max_dyn_loss, diffTicks
) bNeedExit
= Falseusertag
= '' stop_ticks
= self.stoppl
[ "stop_ticks" ] track_threshold
= self.stoppl
[ "track_threshold" ] fallback_boundary
= self.stoppl
[ "fallback_boundary" ] if diffTicks
<= stop_ticks:context.stra_log_text
( "浮虧%.0f超過%d跳,止損離場" %
( diffTicks, stop_ticks
)) bNeedExit
= Trueusertag
= "stoploss" elif self._max_dyn_prof
>= track_threshold and diffTicks
<= fallback_boundary:context.stra_log_text
( "浮贏回撤%.0f->%.0f[閾值%.0f->%.0f],止盈離場" %
( self._max_dyn_prof, diffTicks, track_threshold, fallback_boundary
)) bNeedExit
= Trueusertag
= "stopprof" if bNeedExit:targetprice
= self.get_price
( newTick, -1
) if isLong
else self.get_price
( newTick, 1
) ids
= context.stra_sell
( self.__code__, targetprice, abs
( curPos
) , usertag
) if isLong
else context.stra_buy
( self.__code__, price, abs
( curPos
) , usertag
) for localid
in ids:self.__orders__
[ localid
] = localid
return now
= makeTime
( self.__ctx__.stra_get_date
( ) , self.__ctx__.stra_get_time
( ) , self.__ctx__.stra_get_secs
( )) if newTick
[ "volumn" ] == 0 and self._last_atp__
== 0.0:
return if self.__last_entry_time__ is not None and self.__freq__
!= 0:span
= now - self.__last_entry_time__
if span.total_seconds
( ) <= self.__freq__:
return hisTicks
= context.stra_get_ticks
( self.__code__, self.count + 1
) if hisTicks.size
!= self.count+1:
return if ( len
( newTick
[ "askprice" ] ) == 0
) or
( len
( newTick
[ "bidprice" ] ) == 0
) :
return spread
= newTick
[ "askprice" ] [ 0
] - newTick
[ "bidprice" ] [ 0
] total_OIR
= 0.0total_rou
= 0.0
for i
in range
( 1, self.count + 1
) :prevTick
= hisTicks.get_tick
( i-1
) curTick
= hisTicks.get_tick
( i
) lastBidPx
= self.get_price
( prevTick, -1
) lastAskPx
= self.get_price
( prevTick, 1
) lastBidQty
= prevTick
[ "bidqty" ] [ 0
] if len
( prevTick
[ "bidqty" ] ) > 0
else 0lastAskQty
= prevTick
[ "askqty" ] [ 0
] if len
( prevTick
[ "askqty" ] ) > 0
else 0curBidPx
= self.get_price
( curTick, -1
) curAskPx
= self.get_price
( curTick, 1
) curBidQty
= curTick
[ "bidqty" ] [ 0
] if len
( curTick
[ "bidqty" ] ) > 0
else 0curAskQty
= curTick
[ "askqty" ] [ 0
] if len
( curTick
[ "askqty" ] ) > 0
else 0delta_vb
= 0.0delta_va
= 0.0
if curBidPx
< lastBidPx:delta_vb
= 0.0
elif curBidPx
== lastBidPx:delta_vb
= curBidQty - lastBidQtyelse:delta_vb
= curBidQty
if curAskPx
< lastAskPx:delta_va
= curAskQty
elif curAskPx
== lastAskPx:delta_va
= curAskQty - lastAskQtyelse:delta_va
= 0.0voi
= delta_vb - delta_vatotal_OIR +
= self.beta_oi
[ i-1
] *voi/spreadrou
= ( curBidQty - curAskQty
) /
( curBidQty + curAskQty
) total_rou +
= self.beta_rou
[ i-1
] *rou/spreadprevTick
= hisTicks.get_tick
( -2
) prevMP
= ( self.get_price
( prevTick, -1
) + self.get_price
( prevTick, 1
)) /2curMP
= ( newTick
[ "askprice" ] [ 0
] + newTick
[ "bidprice" ] [ 0
] ) /2
if newTick
[ "volumn" ] != 0:avgTrdPx
= newTick
[ "turn_over" ] /newTick
[ "volumn" ] /self.__comm_info__.volscale
elif self._last_atp__
!= 0:avgTrdPx
= self._last_atp__else:avgTrdPx
= curMPself._last_atp__
= avgTrdPxcurR
= avgTrdPx -
( prevMP + curMP
) / 2efpc
= self.beta_0 + total_OIR + total_rou + self.beta_r * curR / spread
if efpc
>= self.threshold:targetPos
= self.__lots__diffPos
= targetPos - curPos
if diffPos
!= 0.0:targetPx
= newTick
[ "askprice" ] [ 0
] ids
= context.stra_buy
( self.__code__, targetPx, abs
( diffPos
) ,
"enterlong" ) for localid
in ids:self.__orders__
[ localid
] = localidself.__last_entry_time__
= nowself._max_dyn_prof
= 0self._max_dyn_loss
= 0
elif efpc
<= -self.threshold:targetPos
= -self.__lots__diffPos
= targetPos - curPos
if diffPos
!= 0:targetPx
= newTick
[ "bidprice" ] [ 0
] ids
= context.stra_sell
( self.__code__, targetPx, abs
( diffPos
) ,
"entershort" ) for localid
in ids:self.__orders__
[ localid
] = localidself.__last_entry_time__
= nowself._max_dyn_prof
= 0.0self._max_dyn_loss
= 0.0def on_bar
( self, context:HftContext, stdCode:str, period:str, newBar:dict
) :
return def on_channel_ready
( self, context:HftContext
) :undone
= context.stra_get_undone
( self.__code__
) if undone
!= 0 and len
( self.__orders__.keys
( )) == 0:context.stra_log_text
( "%s存在不在管理中的未完成單%f手,全部撤銷" %
( self.__code__, undone
)) isBuy
= ( undone
> 0
) ids
= context.stra_cancel_all
( self.__code__, isBuy
) for localid
in ids:self.__orders__
[ localid
] = localidself.__cancel_cnt__ +
= len
( ids
) context.stra_log_text
( "在途撤單數 -> %d" %
( self.__cancel_cnt__
)) self.__channel_ready__
= Truedef on_channel_lost
( self, context:HftContext
) :context.stra_log_text
( "交易通道連接丟失" ) self.__channel_ready__
= Falsedef on_entrust
( self, context:HftContext, localid:int, stdCode:str, bSucc:bool, msg:str, userTag:str
) :
return def on_order
( self, context:HftContext, localid:int, stdCode:str, isBuy:bool, totalQty:float, leftQty:float, price:float, isCanceled:bool, userTag:str
) :
if localid not
in self.__orders__:
return if isCanceled or leftQty
== 0:self.__orders__.pop
( localid
) if self.__cancel_cnt__
> 0:self.__cancel_cnt__ -
= 1self.__ctx__.stra_log_text
( "在途撤單數 -> %d" %
( self.__cancel_cnt__
)) return def on_trade
( self, context:HftContext, localid:int, stdCode:str, isBuy:bool, qty:float, price:float, userTag:str
) :self._last_entry_price
= price
模型回測 模型編碼完成以后,我們就可以考慮模型回測了。
數據準備 筆者共享了股指期貨主力合約2020年12月到2021年1月份的tick數據到百度網盤中,地址如下: https://pan.baidu.com/s/1Bdxh_PgjqHMzuGjl9ernhg 提取碼:d6bh
文件名為CFFEX.IF.HOT_ticks_20201201_20210118.7z,讀者可以自行獲取。
數據格式為WonderTrader內部壓縮存放的數據格式.dsb,如果要做回歸的話,那么還需要將.dsb文件導出為csv文件。wtpy中的WtDtHelper模塊中就提供了數據轉換的方法,調用代碼如下:
from wtpy.wrapper
import WtDataHelper
import osdtHelper
= WtDataHelper
( )
dtHelper.dump_ticks
( 'dsb文件所在的目錄' ,
'要輸出的csv目錄' )
csv數據導出以后,就可以利用python讀取數據進行模型線性回歸了。
回測入口 線性回歸做好以后,得到一組系數。然后編寫回測入口腳本,代碼如下:
from wtpy
import WtBtEngine, EngineType
from strategies.HftStraOrdImbal
import HftStraOrderImbalancedef read_params_from_csv
( filename
) -
> dict:params
= { "beta_0" :0.0,
"beta_r" :0.0,
"beta_oi" :
[ ] ,
"beta_rou" :
[ ] } f
= open
( filename,
"r" ) lines
= f.readlines
( ) f.close
( ) for row
in range
( 1, len
( lines
)) :curLine
= lines
[ row
] ay
= curLine.split
( "," ) if row
== 1:params
[ "beta_0" ] = float
( ay
[ 1
] ) elif row
== 14:params
[ "beta_r" ] = float
( ay
[ 1
] ) elif row
> 1 and row
<= 7:params
[ "beta_oi" ] .append
( float
( ay
[ 1
] )) elif row
> 7 and row
<= 13:params
[ "beta_rou" ] .append
( float
( ay
[ 1
] )) return params
if __name__
== "__main__" : engine
= WtBtEngine
( EngineType.ET_HFT
) engine.init
( '.\\Common\\' ,
"configbt.json" ) engine.configBacktest
( 202101040900,202101181500
) engine.configBTStorage
( mode
= "csv" , path
= "./storage/" ) engine.commitBTConfig
( ) active_sections
= [ { "start" : 931,
"end" : 1457
} ] stop_params
= { "active" :True,
"stop_ticks" : -25,
"track_threshold" : 15,
"fallback_boundary" : 2,
"calc_price" :0
} params
= read_params_from_csv
( 'IF_10ticks_20201201_20201231.csv' ) straInfo
= HftStraOrderImbalance
( name
= 'hft_IF' ,code
= "CFFEX.IF.HOT" ,count
= 6,lots
= 1,threshold
= 0.3,expsecs
= 5,offset
= 0,freq
= 0,active_secs
= active_sections,stoppl
= stop_params,**params
) engine.set_hft_strategy
( straInfo
) engine.run_backtest
( ) kw
= input
( 'press any key to exit\n' ) engine.release_backtest
( ) 回測結果
我們使用2020年12月的全部tick進行線性回歸,得到的參數用于2021年1月回測得到的績效如下:
date,closeprofit,positionprofit,dynbalance,fee
20210104,-11160.00,0.00,-20941.01,9781.01
20210105,-20100.00,0.00,-40712.85,20612.85
20210106,-60.00,0.00,-31828.36,31768.36
20210107,4140.00,0.00,-40344.73,44484.73
20210108,-11760.00,0.00,-66329.60,54569.60
20210111,-41280.00,0.00,-107444.80,66164.80
20210112,-66000.00,0.00,-142723.28,76723.28
20210113,-87240.00,0.00,-175926.18,88686.18
20210114,-106680.00,0.00,-202219.21,95539.21
20210115,-96840.00,0.00,-197721.78,100881.78
20210118,-110760.00,0.00,-219671.31,108911.31
從上面的績效可以看出,該模型的表現倒是比較穩定,可惜是穩定的虧錢[手動狗頭],實在是難堪大用。
績效分析 策略表現雖然難以入目,但是我們還是要進行績效分析,看看有沒有可以改進的點。WonderTrader針對HFT回測生成了回合明細closes.csv,可以看到每個回合的進場點和出場點,以及每個回合潛在最大收益和潛在最大虧損。用戶可以利用回合明細根據需求自行分析每個回合進出場的點位是否合理,以及如何優化等問題。
code,direct,opentime,openprice,closetime,closeprice,qty,profit,maxprofit,maxloss,totalprofit,entertag,exittag
CFFEX.IF.HOT,SHORT,20210104093156400,5221,20210104093218400,5221,1,-0,480,-540,0,entershort,enterlong
CFFEX.IF.HOT,LONG,20210104093218400,5221,20210104093219900,5222,1,300,300,0,300,enterlong,entershort
CFFEX.IF.HOT,SHORT,20210104093219900,5222,20210104093226900,5223,1,-300,120,-480,0,entershort,enterlong
CFFEX.IF.HOT,LONG,20210104093226900,5223,20210104093301400,5216.8,1,-1860,240,-2040,-1860,enterlong,stoploss
CFFEX.IF.HOT,SHORT,20210104093317400,5210.8,20210104093319900,5211.2,1,-120,0,-480,-1980,entershort,enterlong
CFFEX.IF.HOT,LONG,20210104093320400,5210.6,20210104093347900,5211.4,1,240,540,-1080,-1740,enterlong,entershort
CFFEX.IF.HOT,SHORT,20210104093347900,5211.4,20210104093410900,5211,1,120,660,-480,-1620,entershort,enterlong
CFFEX.IF.HOT,LONG,20210104093410900,5211,20210104093424400,5203.4,1,-2280,0,-2460,-3900,enterlong,stoploss
CFFEX.IF.HOT,SHORT,20210104093432900,5201.2,20210104093446900,5207.2,1,-1800,120,-2040,-5700,entershort,stoploss
結束語 到此為止,一個完整的HFT策略開發流程就走完了。雖然該模型似乎已經失效,但是筆者并沒有深入分析當前IF的市場和原模型回測的時間區間的IF的市場之間的差別,另外筆者也沒有拓展到別的品種進行分析。再者,筆者的主要目的是演示HFT策略的研發流程,所以關于模型方面難免有所疏漏。模型方面的做法,請各位讀者稍作參考即可。
值得一提的是,從上面的源碼中可以看到,WonderTrader針對HFT策略的交易接口簡化成了買、賣兩個交易接口,目的就是為了簡化策略開發的邏輯,讓策略人研發人員將更多的精力集中在策略邏輯本身。而買賣對應的開平邏輯,會在C++核心通過配置文件actionpolicy.json進行控制,自動處理開平。另外,該策略使用Python開發,而C++版本的相同策略,回測時間約為Python版本的十分之一左右,如果有讀者想要利用WonderTrader上高頻,在開發語言方面,還請各位讀者仔細斟酌。
筆者也會不斷地完善WonderTrader在HFT策略方面的功能。也希望各位讀者能多多指正WonderTrader的疏漏,幫助WonderTrader完善起來,也能為更多的用戶提供更好的基礎設施服務。
最后再來一波廣告
WonderTrader的github地址:https://github.com/wondertrader/wondertrader
WonderTrader官網地址:https://wondertrader.github.io
wtpy的github地址:https://github.com/wondertrader/wtpy
總結
以上是生活随笔 為你收集整理的WonderTrader高频交易初探及v0.6发布 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。