ITPub博客

首页 > 应用开发 > Python > VNPY,从发送交易指令到交易所的源代码分析

VNPY,从发送交易指令到交易所的源代码分析

原创 Python 作者:张国平 时间:2018-07-25 21:46:07 0 删除 编辑

在尝试写tick级别的策略,由于交易反馈时间要求高,感觉需要对单个order的事有个全面了解,就花了些时间尝试性去分析了VNPY中 从发送交易指令(sendOrder())到交易所,和接收成交返回信息(onOrder()/onTrade())的代码。如果有错误或者遗漏,请指正。这里先将发送

  1. 在策略中,一般不直接调用sendOrder(), 而且用四个二次封装函数函数,这些都是在class      CtaTemplate中定义的,这里面主要区别就是sendorder()函数的中ordertype制定不一样,用来区分是买开卖开等交易类型。

    返回一个vtOrderIDList, 这个list里面包含vtOrderID,这个是个内部给号,可以用做追踪同一个order的状态。

    def buy(self, price, volume, stop=False):
        """买开"""
        return self.sendOrder(CTAORDER_BUY, price, volume, stop)
    
    #----------------------------------------------------------------------
    def sell(self, price, volume, stop=False):
        """卖平"""
        return self.sendOrder(CTAORDER_SELL, price, volume, stop)       
 
    #----------------------------------------------------------------------
    def short(self, price, volume, stop=False):
        """卖开"""
        return self.sendOrder(CTAORDER_SHORT, price, volume, stop)          
 
    #----------------------------------------------------------------------
    def cover(self, price, volume, stop=False):
        """买平"""
        return self.sendOrder(CTAORDER_COVER, price, volume, stop)


2.  接下来我们看看那sendOrder()源码,还在class CtaTemplate中定义;如果stop为True是本地停止单,这个停止单并没有发送给交易所,而是存储在内部,使用ctaEngine.sendStopOrder()函数; 否则这直接发送到交易所,使用ctaEngine.sendStopOrder函数。

      这里会返回一个vtOrderIDList, 这个list里面包含vtOrderID,然后在被上面返回。这里补充一下,对于StopOrder真正触发的交易通常是涨停价或者跌停价发出的市价单(Market price),参数price只是触发条件;而普通sendOrder是真正按照参数price的限价单(Limit price)

    def sendOrder(self, orderType, price, volume, stop=False):
        """发送委托"""
        if self.trading:
            # 如果stop为True,则意味着发本地停止单
            if stop:
                vtOrderIDList = self.ctaEngine.sendStopOrder(self.vtSymbol, orderType, price, volume, self)
            else:
                vtOrderIDList = self.ctaEngine.sendOrder(self.vtSymbol, orderType, price, volume, self)
            return vtOrderIDList
        else:
            # 交易停止时发单返回空字符串
            return []


3. 这里我们首先看看ctaEngine.sendStopOrder()函数,在class CtaEngine中定义的,首先实例初始化时候定义了两个字典,用来存放stoporder,区别一个是停止单撤销后删除,一个不会删除;还定义了一个字典,策略对应的所有orderID。

def __init__(self, mainEngine, eventEngine):
   ………
       # 本地停止单字典
       # key为stopOrderID,value为stopOrder对象
       self.stopOrderDict = {}             # 停止单撤销后不会从本字典中删除
       self.workingStopOrderDict = {}      # 停止单撤销后会从本字典中删除
  
   # 保存策略名称和委托号列表的字典
   # key为name,value为保存orderID(限价+本地停止)的集合
   self.strategyOrderDict = {}
    ………

然后在函数 sendStopOrder 中,首先记录给本地停止单一个专门编号,就是前缀加上顺序编号,其中STOPORDERPREFIX 是 'CtaStopOrder.',那么第一条本地编码就是 ' CtaStopOrder. 1' 后面是这个单据信息;这里可以发现 orderType 其实是一个 direction offset 的组合,交易方向 direction Long short 两个情况,交易对 offset open close 两个情况。组合就是上面买开,卖平等等。然后把这个 stoporder 放入字典,等待符合价格情况到达触发真正的发单。这里返回本地编码作为 vtOrderIDList

def sendStopOrder(self, vtSymbol, orderType, price, volume, strategy):
        """发停止单(本地实现)"""
        self.stopOrderCount += 1
        stopOrderID = STOPORDERPREFIX + str(self.stopOrderCount)
        
        so = StopOrder()
        so.vtSymbol = vtSymbol
        so.orderType = orderType
        so.price = price
        so.volume = volume
        so.strategy = strategy
        so.stopOrderID = stopOrderID
        so.status = STOPORDER_WAITING
        
        if orderType == CTAORDER_BUY:
            so.direction = DIRECTION_LONG
            so.offset = OFFSET_OPEN
        elif orderType == CTAORDER_SELL:
            so.direction = DIRECTION_SHORT
            so.offset = OFFSET_CLOSE
        elif orderType == CTAORDER_SHORT:
            so.direction = DIRECTION_SHORT
            so.offset = OFFSET_OPEN
        elif orderType == CTAORDER_COVER:
            so.direction = DIRECTION_LONG
            so.offset = OFFSET_CLOSE           
        
        # 保存stopOrder对象到字典中
        self.stopOrderDict[stopOrderID] = so
        self.workingStopOrderDict[stopOrderID] = so
        
        # 保存stopOrderID到策略委托号集合中
        self.strategyOrderDict[strategy.name].add(stopOrderID)
        
        # 推送停止单状态
        strategy.onStopOrder(so)
        
        return [stopOrderID]

4.  下面是processStopOrder () 函数,也在 class CtaEngine中定义的,主要是当行情符合时候如何发送真正交易指令,因为 stopOrderID 不是 tick 交易重点,这里简单讲讲,具体请看源码。

当接收到 tick 时候,会查看 tick.vtSymbol ,是不是存在 workingStopOrderDict so .vtSymbol 有一样的,如果有,再看 tick.lastPrice 价格是否可以满足触发阈值,如果满足,根据原来 so 的交易 Direction Long 按照涨停价, Short 按照跌停价发出委托。然后从 workingStopOrderDic 和strategyOrderDict移除该 so ,并更新 so 状态,并触发事件 onStopOrder(so).

这里发现,so只是只是按照涨停价发单给交易所,并没有确保成绩,而且市价委托的实际交易vtOrderID也没有返回;从tick交易角度,再收到tick后再发送交易,本事也是有了延迟一tick。所以一般tick级别交易不建议使用stoporder。

   def processStopOrder(self, tick):
        """收到行情后处理本地停止单(检查是否要立即发出)"""
        vtSymbol = tick.vtSymbol
        
        # 首先检查是否有策略交易该合约
        if vtSymbol in self.tickStrategyDict:
            # 遍历等待中的停止单,检查是否会被触发
            for so in self.workingStopOrderDict.values():
                if so.vtSymbol == vtSymbol:
                    longTriggered = so.direction==DIRECTION_LONG and tick.lastPrice>=so.price        # 多头停止单被触发
                    shortTriggered = so.direction==DIRECTION_SHORT and tick.lastPrice<=so.price     # 空头停止单被触发
                    
                    if longTriggered or shortTriggered:
                        # 买入和卖出分别以涨停跌停价发单(模拟市价单)
                        if so.direction==DIRECTION_LONG:
                            price = tick.upperLimit
                        else:
                            price = tick.lowerLimit
                        
                        # 发出市价委托
                        self.sendOrder(so.vtSymbol, so.orderType, price, so.volume, so.strategy)
                        
                        # 从活动停止单字典中移除该停止单
                        del self.workingStopOrderDict[so.stopOrderID]
                        
                        # 从策略委托号集合中移除
                        s = self.strategyOrderDict[so.strategy.name]
                        if so.stopOrderID in s:
                            s.remove(so.stopOrderID)
                        
                        # 更新停止单状态,并通知策略
                        so.status = STOPORDER_TRIGGERED
                        so.strategy.onStopOrder(so)

5.  前面说了这么多,终于到了正主 sendOrder(), 也在 class CtaEngine中定义的。代码较长,下面做了写缩减。

1 )通过mainEngine.getContract获得这个品种的合约的信息,包括这个合约的名称,接口名 gateway (国内期货就是 ctp ),交易所,最小价格变动等信息;

2 )创建一个 class VtOrderReq的对象 req ,在vtObject.py中,这个 py 包括很多事务类的定义;然后赋值,包括 contract 获得信息,交易手数,和 price ,和 priceType ,这里只有限价单。

3 )根据 orderType 赋值 direction offset ,之前 sendStopOrder 中已经说了,就不重复。

4 )然后跳到 mainEngine.convertOrderReq(req) ,这里代码比较跳,分析下来,如果之前没有持仓,或者是直接返回 [req] ;如果有持仓就调用PositionDetail . convertOrderReq(req) ,这个时候如果是平仓操作,就分析持仓量,和平今和平昨等不同操作返回拆分的出来 [ reqTd , reqYd ] ,这里不展开。

5) 如果上一部没有返回 [req] ,则委托有问题,直接返回控制。如果有 [req] ,因为存在多个 req 情况,就遍历每个 req ,使用 mainEngine.sendOrder 发单,并保存返回的 vtOrderID orderStrategyDict [], strategyOrderDict [] 两个字典;然后把 vtOrderIDList 返回。

    def sendOrder(self, vtSymbol, orderType, price, volume, strategy):
        """发单"""
        contract = self.mainEngine.getContract(vtSymbol)
        
        req = VtOrderReq()
        req.symbol = contract.symbol
    ……
        
        # 设计为CTA引擎发出的委托只允许使用限价单
        req.priceType = PRICETYPE_LIMITPRICE    
        
        # CTA委托类型映射
        if orderType == CTAORDER_BUY:
            req.direction = DIRECTION_LONG
            req.offset = OFFSET_OPEN
        ……
            
        # 委托转换
        reqList = self.mainEngine.convertOrderReq(req)
        vtOrderIDList = []
        
        if not reqList:
            return vtOrderIDList
        
        for convertedReq in reqList:
            vtOrderID = self.mainEngine.sendOrder(convertedReq, contract.gatewayName)    # 发单
            self.orderStrategyDict[vtOrderID] = strategy                                 # 保存vtOrderID和策略的映射关系
            self.strategyOrderDict[strategy.name].add(vtOrderID)                         # 添加到策略委托号集合中
            vtOrderIDList.append(vtOrderID)
            
        self.writeCtaLog(u'策略%s发送委托,%s,%s,%s@%s' 
                         %(strategy.name, vtSymbol, req.direction, volume, price))
        
        return vtOrderIDList

6.    在mainEngine.sendOrder中,这里不列举代码了,首先进行风控,如果到阈值就不发单,然后看 gateway 是否存在,如果存在,就调用 gateway. sendOrder(orderReq)方法;下面用 ctpgateway 说明。class      CtpGateway(VtGateway)是 VtGateway 是继承,把主要发单,返回上面都实现,同时对于不同的接口,比如外汇,数字货币,只要用一套接口标准就可以,典型继承使用。    

CtpGateway.sendOrder实际是调用class CtpTdApi(TdApi)的,这个就是一套ctp交易交口,代码很简单,最后是调用封装好C++的ctp接口reqOrderInsert()。最关键返回的vtOrderID是接口名+顺序数。

    def sendOrder(self, orderReq):
        """发单"""
        self.reqID += 1
        self.orderRef += 1
       
        req = {}
       
        req['InstrumentID'] = orderReq.symbol
        req['LimitPrice'] = orderReq.price
        req['VolumeTotalOriginal'] = orderReq.volume
       
        # 下面如果由于传入的类型本接口不支持,则会返回空字符串
        req['OrderPriceType'] = priceTypeMap.get(orderReq.priceType, '')
        .......
       
        # 判断FAK和FOK
        if orderReq.priceType == PRICETYPE_FAK:
            req['OrderPriceType'] = defineDict["THOST_FTDC_OPT_LimitPrice"]
            req['TimeCondition'] = defineDict['THOST_FTDC_TC_IOC']
            req['VolumeCondition'] = defineDict['THOST_FTDC_VC_AV']
        if orderReq.priceType == PRICETYPE_FOK:
            req['OrderPriceType'] = defineDict["THOST_FTDC_OPT_LimitPrice"]
            req['TimeCondition'] = defineDict['THOST_FTDC_TC_IOC']
            req['VolumeCondition'] = defineDict['THOST_FTDC_VC_CV']       
       
        self.reqOrderInsert(req, self.reqID)
       
        # 返回订单号(字符串),便于某些算法进行动态管理
        vtOrderID = '.'.join([self.gatewayName, str(self.orderRef)])
        return vtOrderID

整个流程下来,不考虑stoporder,是ctaTemplate -> CtaEngine ->mainEngine ->ctpgateway ->CtpTdApi, 传到C++封装的接口。返回的就是vtOrderID; 因为存在平昨,平今还有锁仓,反手等拆分情况,返回的可能是一组。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/22259926/viewspace-2158790/,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
SAP 金融风险管理产品专家

注册时间:2009-08-05

  • 博文量
    134
  • 访问量
    322166