VNPY,從傳送交易指令到交易所的原始碼分析

張國平發表於2018-07-25

在嘗試寫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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章