在嘗試寫tick級别的政策,由于交易回報時間要求高,感覺需要對單個order的事有個全面了解,就花了些時間嘗試性去分析了VNPY中 從發送交易指令(sendOrder())到交易所,和接收成交傳回資訊(onOrder()/onTrade())的代碼。如果有錯誤或者遺漏,請指正。這裡先将發送
在政策中,一般不直接調用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; 因為存在平昨,平今還有鎖倉,反手等拆分情況,傳回的可能是一組。