參考:用Python的交易員
【前言】從本篇教程開始,所有的開發都會在Python環境中進行(謝天謝地可以和C++說再見了)。
一、底層接口簡介
1、通常情況下,一個交易程序的架構會由以下三個部分組成:
-
底層接口:負責對接行情和交易API,將數據推送到系統核心中,以及發送指令(下單、數據請求等)
-
中層引擎:用於整合程序中的各個組件(包括底層接口、數據庫接口等等)到一個對象中,便於頂層UI調用
-
頂層GUI:用於顯示數據和調用中層引擎暴露的主動函數,實現各項具體功能
2、上面這張圖展示的是國外的一款開源交易平台AlgoTrader的架構:
-
兩邊的Adapters代表的是底層接口(左邊行情數據,右邊交易)
-
紅色圓柱形中包括的是中層引擎架構,事件驅動方面使用了Esper復雜事件處理(CEP)引擎,同時內置了一些常用的功能引擎,如期權定價引擎、外匯對沖模塊、投資組合管理模塊等
-
上方的Strategy1、2等代表的是頂層應用(算法策略、GUI界面等),通過調用中層引擎的功能來實現用戶所需的業務
3、vn.py和AlgoTrader的比較:
這里對兩個項目做一個簡單的比較。
vn.py優勢:
-
語言易用:Java語言比Python啰嗦
-
架構簡潔:Java的編程理念(純面向對象,大量使用框架等)更是比Python的編程理念(人生苦短,我們的目標是搞定問題)繁瑣
-
事件驅動引擎:AlgoTrader使用的Esper引擎盡管功能強大,使用起來也過於復雜,對於國內絕大部分的量化業務而言完全用不着
-
本地化:vn.py完全為中國市場設計,在功能設計上更符合國人的使用習慣,而AlgoTrader則是針對歐美市場設計
AlgoTrader優勢:
-
靜態語言:Java在開發時可以進行靜態檢查,同時相對較低的靈活性使得其更適合大型團隊使用(即每個成員未必對項目整體非常了解)
-
國外接口:已有大量的國外經紀商和行情提供商接口,如果用戶主要做歐美市場基本可以開箱即用
-
成熟度:AlgoTrader從2009發布至今已經有6年的歷史,同時有着相當數量的機構客戶
兩個項目的相似之處:
-
作者在最初都是為了交易某種期權策略而開發了該項目
-
整體框架設計類似(底層接口、中層引擎、頂層GUI)
-
都可以非常方便的開發全自動交易策略
-
都是開源項目,目前托管在Github上,用戶可以根據自己的需求隨意定制相關功能
-
都可以應用於高頻交易(毫秒級延遲),不適用於超高頻交易(微秒級延遲)
4、教程說明
本篇教程將會介紹底層接口的開發,后面的若干篇則是關於中層引擎和各種頂層GUI組件。相關的示例都是基於vn.demo中的LTS接口DEMO,發布在:https://github.com/vnpy/vnpy/tree/master/vn.demo/ltsdemo
二、底層接口對接
通過前面的教程,我們已經獲得了和原生C++ API功能完全相同的Python封裝API。通常情況下,為了將某個API對接到我們的程序中,需要以下兩步:
-
將API的回調函數收到的數據推送到程序的中層引擎中,等待處理
-
將API的主動函數進行一定的簡化封裝,便於中層引擎調用
vn.lts中的API接口在使用時需要由用戶繼承后實現回調函數對應的具體功能,下面的內容以行情接口為例。
######################################################################## class DemoMdApi(MdApi): """ Demo中的行情API封裝 封裝后所有數據自動推送到事件驅動引擎中,由其負責推送到各個監聽該事件的回調函數上 對用戶暴露的主動函數包括: 登陸 login 訂閱合約 subscribe """ #---------------------------------------------------------------------- def __init__(self, eventEngine): """ API對象的初始化函數 """ super(DemoMdApi, self).__init__() # 事件引擎,所有數據都推送到其中,再由事件引擎進行分發 self.__eventEngine = eventEngine # 請求編號,由api負責管理 self.__reqid = 0 # 以下變量用於實現連接和重連后的自動登陸 self.__userid = '' self.__password = '' self.__brokerid = '' # 以下集合用於重連后自動訂閱之前已訂閱的合約,使用集合為了防止重復 self.__setSubscribed = set() # 初始化.con文件的保存目錄為\mdconnection,注意這個目錄必須已存在,否則會報錯 self.createFtdcMdApi(os.getcwd() + '\\mdconnection\\')
-
DemoMdApi類繼承自MdApi類,並實現了回調函數的具體功能。
-
創建DemoMdApi的對象時,用戶需要傳入的參數是事件驅動引擎對象eventEngine。
-
每次調用API的主動函數時,需要傳入一個reqid的參數,作為本次請求的唯一標識,絕大部分情況下我們不需要在意每個請求的標識情況,因此選擇將該參數交給DemoMdApi對象來維護,每次調用主動函數時自動加1。
-
我們在DemoMdApi的對象中保存用戶名、密碼和經紀商編號,用於前置機連接完成后的自動登錄功能,以及斷線重連相關的操作。
-
__setSubscribed對應的是一個Python集合,用於保存我們通過訂閱函數訂閱過的合約,在斷線重連后自動進行訂閱,之所以選擇set而不是list是為了保證合約的唯一性,避免重復訂閱(盡管重復訂閱也沒影響)。
-
在創建對象DemoMdApi對象的同時,自動調用createFtdcMdApi來初始化連接接口,選擇使用當前目錄下的mdconnection文件夾來保存.con通訊文件。
1、回調函數
#---------------------------------------------------------------------- def onFrontConnected(self): """服務器連接""" event = Event(type_=EVENT_LOG) event.dict_['log'] = u'行情服務器連接成功' self.__eventEngine.put(event) # 如果用戶已經填入了用戶名等等,則自動嘗試連接 if self.__userid: req = {} req['UserID'] = self.__userid req['Password'] = self.__password req['BrokerID'] = self.__brokerid self.__reqid = self.__reqid + 1 self.reqUserLogin(req, self.__reqid) ... #---------------------------------------------------------------------- def onRspUserLogin(self, data, error, n, last): """登陸回報""" event = Event(type_=EVENT_LOG) if error['ErrorID'] == 0: log = u'行情服務器登陸成功' else: log = u'登陸回報,錯誤代碼:' + unicode(error['ErrorID']) + u',' + u'錯誤信息:' + error['ErrorMsg'].decode('gbk') event.dict_['log'] = log self.__eventEngine.put(event) # 重連后自動訂閱之前已經訂閱過的合約 if self.__setSubscribed: for instrument in self.__setSubscribed: self.subscribe(instrument[0], instrument[1]) ... #---------------------------------------------------------------------- def onRtnDepthMarketData(self, data): """行情推送""" # 行情推送收到后,同時觸發常規行情事件,以及特定合約行情事件,用於滿足不同類型的監聽 # 常規行情事件 event1 = Event(type_=EVENT_MARKETDATA) event1.dict_['data'] = data self.__eventEngine.put(event1) # 特定合約行情事件 event2 = Event(type_=(EVENT_MARKETDATA_CONTRACT+data['InstrumentID'])) event2.dict_['data'] = data self.__eventEngine.put(event2) ...
-
通過回調函數收到API的數據推送后,創建不同類型的事件Event對象(來自於事件驅動引擎模塊),在事件對象的數據字典dict_中保存需要具體推送的數據,然后推送到事件驅動引擎中,由其負責處理。
-
回調函數收到的數據中,data和error分別對應的是保存主要數據(如行情)和錯誤信息的字典,n是該回調函數對應的請求號(即調用主動函數時的reqid),last是一個布爾值,代表是否為該次調用的最后返回信息。
-
我們主要對data字典感興趣,因此選擇在事件中整體推送。
-
而error字典每次收到后應當立即檢查是否包含錯誤信息(因為即使沒有發生錯誤也會推送),若有則自動保存為一個日志事件(通過日志監控控件顯示出來)。
-
服務器連接完成后(onFrontConnected),檢查是否已經填入了用戶名等登錄信息,若有則自動登錄(請參考后面主動函數中的示例)。
-
登陸完成后(onRspUserLogin),自動訂閱__setSubscribed中之前已經訂閱過的合約。
-
收到行情推送后(onRtnDepthMarketData),我們選擇創建兩種事件,一種是常規行情事件(通常適用於市場行情監控GUI等對所有行情推送都關注的組件),另一種是特定合約行情事件(通常適用於算法等僅關注特定合約行情的組件)。
-
當我們調用會有返回信息的主動函數時,需要傳入本次請求的編號,此時我們先將__reqid自加1,再作為參數傳入主動函數中。
2、主動函數
#---------------------------------------------------------------------- def login(self, address, userid, password, brokerid): """連接服務器""" self.__userid = userid self.__password = password self.__brokerid = brokerid # 注冊服務器地址 self.registerFront(address) # 初始化連接,成功會調用onFrontConnected self.init() #---------------------------------------------------------------------- def subscribe(self, instrumentid, exchangeid): """訂閱合約""" req = {} req['InstrumentID'] = instrumentid req['ExchangeID'] = exchangeid self.subscribeMarketData(req) instrument = (instrumentid, exchangeid) self.__setSubscribed.add(instrument)
-
主動函數僅封裝了兩個功能,登錄login和訂閱合約subscribe。這里假設通常我們不會做登出(直接殺進程)和退訂合約(不一定)之類的操作,有需求的話可以自行封裝對應的函數。
-
對於登錄函數login而言,傳入4個參數包括服務器前置機地址address,用戶名userid,密碼password以及經紀商代碼brokerid。函數調用后,我們先將userid,password和brokerid保存下來,然后注冊服務器地址registerFront,並初始化連接init。連接完成后,onFrontConnected回調函數會被自動調用,然后發生的操作請參考前一段落的回調函數工作流程。
-
LTS的API在訂閱行情時,需要傳入合約的代碼以及合約所在的交易所(因為存在兩個證券交易所相同代碼的情況),而CTP的API在期貨方面則不存在該問題,只需傳入合約代碼。發送訂閱請求后,將該訂閱請求保存在__setSubscribed集合中,使得斷線重連時可以自動重新訂閱。
三、總結
在交易程序的開發中,所有的API對接原理均大同小異,除了類CTP API以外,國內的恆生接口、FIX引擎接口等等也可以同樣遵照以上的原理進行對接設計。
文章中的例子是行情接口,交易接口因為包含了更多的回調函數和主動函數,在設計上相對更為復雜,感興趣讀者建議直接閱讀demo中的源代碼,相關問題可以在vn.py框架交流群(群號:262656087)中提問。