前言
自从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__ = name def 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 #回溯tick条数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: returnif (len(newTick["askprice"]) == 0) or (len(newTick["bidprice"]) == 0): returnspread = 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 0 lastAskQty = prevTick["askqty"][0] if len(prevTick["askqty"]) > 0 else 0 curBidPx = self.get_price(curTick, -1) curAskPx = self.get_price(curTick, 1) curBidQty = curTick["bidqty"][0] if len(curTick["bidqty"]) > 0 else 0 curAskQty = curTick["askqty"][0] if len(curTick["askqty"]) > 0 else 0 delta_vb = 0.0 delta_va = 0.0 if curBidPx < lastBidPx: delta_vb = 0.0 elif curBidPx == lastBidPx: delta_vb = curBidQty - lastBidQty else: delta_vb = curBidQty if curAskPx < lastAskPx: delta_va = curAskQty elif curAskPx == lastAskPx: delta_va = curAskQty - lastAskQty else: delta_va = 0.0 voi = delta_vb - delta_va total_OIR += self.beta_oi[i-1]*voi/spread #计算委比 rou = (curBidQty - curAskQty)/(curBidQty + curAskQty) total_rou += self.beta_rou[i-1]*rou/spreadprevTick = hisTicks.get_tick(-2)# t-1时刻的中间价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__.volscaleelif 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 / spreadif 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] = localid self.__last_entry_time__ = now self._max_dyn_prof = 0 self._max_dyn_loss = 0elif 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] = localid self.__last_entry_time__ = now self._max_dyn_prof = 0.0 self._max_dyn_loss = 0.0
止盈止损逻辑
然而对于高频策略,除了外围的进出场逻辑之外,止盈止损逻辑也是十分重要的一部分。本文中的策略采纳固定点位止损+跟踪止盈来作为止盈止损逻辑,代码如下:
# 止盈止损逻辑if curPos != 0 and self.stoppl["active"]: isLong = (curPos > 0) # 首先获取最新的价格,calc_price为0的话,应用对手价计算浮盈,calc_price为1的话,应用最新价计算浮盈 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 = False usertag = '' 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 = True usertag = "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 = True usertag = "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__ = 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
订单治理逻辑
而后,咱们还须要增加一段订单治理的逻辑,代码如下:
def check_orders(self, ctx:HftContext): #如果未实现订单不为空 ord_cnt = len(self.__orders__.keys()) if ord_cnt > 0 and self.__last_entry_time__ is not None: #以后工夫,肯定要从api获取,不然回测会有问题 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__ += 1 ctx.stra_log_text("在途撤复数 -> %d" % (self.__cancel_cnt__))
其余逻辑
除了上述的逻辑之外,咱们还须要解决一些细节问题,如:
- 解决订单回报,用于更新本地订单的状态;
- 解决成交回报,用于更新入场价格,计算浮动盈亏
- 解决交易通道就绪的回报,用于查看是否有不在治理内的未实现单
残缺源码
整个策略的残缺代码如下:
from wtpy import BaseHftStrategyfrom 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 #回溯tick条数 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__ = None self.__to_clear__ = False self._last_entry_price = 0.0 self._max_dyn_prof = 0.0 self._max_dyn_loss = 0.0 self._last_atp__ = 0.0 def is_active(self, curMin:int) -> bool: for sec in self.active_secs: if sec["start"] <= curMin and curMin <= sec["end"]: return True return False def 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__ = context def check_orders(self, ctx:HftContext): #如果未实现订单不为空 ord_cnt = len(self.__orders__.keys()) if ord_cnt > 0 and self.__last_entry_time__ is not None: #以后工夫,肯定要从api获取,不然回测会有问题 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__ += 1 ctx.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__ = 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 # 止盈止损逻辑 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 = False usertag = '' 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 = True usertag = "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 = True usertag = "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()) # 成交量为0且上一个成交均价为0,则须要退出 if newTick["volumn"] == 0 and self._last_atp__ == 0.0: return #如果曾经入场,且有频率限度,则做频率查看 if self.__last_entry_time__ is not None and self.__freq__ != 0: #以后工夫,肯定要从api获取,不然回测会有问题 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.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 0 lastAskQty = prevTick["askqty"][0] if len(prevTick["askqty"]) > 0 else 0 curBidPx = self.get_price(curTick, -1) curAskPx = self.get_price(curTick, 1) curBidQty = curTick["bidqty"][0] if len(curTick["bidqty"]) > 0 else 0 curAskQty = curTick["askqty"][0] if len(curTick["askqty"]) > 0 else 0 delta_vb = 0.0 delta_va = 0.0 if curBidPx < lastBidPx: delta_vb = 0.0 elif curBidPx == lastBidPx: delta_vb = curBidQty - lastBidQty else: delta_vb = curBidQty if curAskPx < lastAskPx: delta_va = curAskQty elif curAskPx == lastAskPx: delta_va = curAskQty - lastAskQty else: delta_va = 0.0 voi = delta_vb - delta_va total_OIR += self.beta_oi[i-1]*voi/spread #计算委比 rou = (curBidQty - curAskQty)/(curBidQty + curAskQty) total_rou += self.beta_rou[i-1]*rou/spread prevTick = hisTicks.get_tick(-2) # t-1时刻的中间价 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 = curMP self._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] = localid self.__last_entry_time__ = now self._max_dyn_prof = 0 self._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] = localid self.__last_entry_time__ = now self._max_dyn_prof = 0.0 self._max_dyn_loss = 0.0 def 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] = localid self.__cancel_cnt__ += len(ids) context.stra_log_text("在途撤复数 -> %d" % (self.__cancel_cnt__)) self.__channel_ready__ = True def on_channel_lost(self, context:HftContext): context.stra_log_text("交易通道连贯失落") self.__channel_ready__ = False def 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__ -= 1 self.__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... 提取码:d6bh
文件名为CFFEX.IF.HOT_ticks_20201201_20210118.7z,读者能够自行获取。
数据格式为WonderTrader外部压缩寄存的数据格式.dsb
,如果要做回归的话,那么还须要将.dsb
文件导出为csv
文件。wtpy
中的WtDtHelper
模块中就提供了数据转换的办法,调用代码如下:
from wtpy.wrapper import WtDataHelperimport osdtHelper = WtDataHelper()dtHelper.dump_ticks('dsb文件所在的目录', '要输入的csv目录')
csv数据导出当前,就能够利用python读取数据进行模型线性回归了。
回测入口
线性回归做好当前,失去一组系数。而后编写回测入口脚本,代码如下:
from wtpy import WtBtEngine, EngineTypefrom 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 paramsif __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,fee20210104,-11160.00,0.00,-20941.01,9781.0120210105,-20100.00,0.00,-40712.85,20612.8520210106,-60.00,0.00,-31828.36,31768.3620210107,4140.00,0.00,-40344.73,44484.7320210108,-11760.00,0.00,-66329.60,54569.6020210111,-41280.00,0.00,-107444.80,66164.8020210112,-66000.00,0.00,-142723.28,76723.2820210113,-87240.00,0.00,-175926.18,88686.1820210114,-106680.00,0.00,-202219.21,95539.2120210115,-96840.00,0.00,-197721.78,100881.7820210118,-110760.00,0.00,-219671.31,108911.31
从下面的绩效能够看出,该模型的体现倒是比较稳定,惋惜是稳固的亏钱[手动狗头],切实是难堪大用。
绩效剖析
策略体现尽管难以入目,然而咱们还是要进行绩效剖析,看看有没有能够改良的点。WonderTrader针对HFT
回测生成了回合明细closes.csv
,能够看到每个回合的进场点和出场点,以及每个回合潜在最大收益和潜在最大亏损。用户能够利用回合明细依据需要自行剖析每个回合进出场的点位是否正当,以及如何优化等问题。
code,direct,opentime,openprice,closetime,closeprice,qty,profit,maxprofit,maxloss,totalprofit,entertag,exittagCFFEX.IF.HOT,SHORT,20210104093156400,5221,20210104093218400,5221,1,-0,480,-540,0,entershort,enterlongCFFEX.IF.HOT,LONG,20210104093218400,5221,20210104093219900,5222,1,300,300,0,300,enterlong,entershortCFFEX.IF.HOT,SHORT,20210104093219900,5222,20210104093226900,5223,1,-300,120,-480,0,entershort,enterlongCFFEX.IF.HOT,LONG,20210104093226900,5223,20210104093301400,5216.8,1,-1860,240,-2040,-1860,enterlong,stoplossCFFEX.IF.HOT,SHORT,20210104093317400,5210.8,20210104093319900,5211.2,1,-120,0,-480,-1980,entershort,enterlongCFFEX.IF.HOT,LONG,20210104093320400,5210.6,20210104093347900,5211.4,1,240,540,-1080,-1740,enterlong,entershortCFFEX.IF.HOT,SHORT,20210104093347900,5211.4,20210104093410900,5211,1,120,660,-480,-1620,entershort,enterlongCFFEX.IF.HOT,LONG,20210104093410900,5211,20210104093424400,5203.4,1,-2280,0,-2460,-3900,enterlong,stoplossCFFEX.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/wondertrad...
WonderTrader官网地址:https://wondertrader.github.io
wtpy的github
地址:https://github.com/wondertrad...