量化交易平台—底層接口對接


參考:用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對接到我們的程序中,需要以下兩步:

  1. 將API的回調函數收到的數據推送到程序的中層引擎中,等待處理

  2. 將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\\')
  1. DemoMdApi類繼承自MdApi類,並實現了回調函數的具體功能。

  2. 創建DemoMdApi的對象時,用戶需要傳入的參數是事件驅動引擎對象eventEngine。

  3. 每次調用API的主動函數時,需要傳入一個reqid的參數,作為本次請求的唯一標識,絕大部分情況下我們不需要在意每個請求的標識情況,因此選擇將該參數交給DemoMdApi對象來維護,每次調用主動函數時自動加1。

  4. 我們在DemoMdApi的對象中保存用戶名、密碼和經紀商編號,用於前置機連接完成后的自動登錄功能,以及斷線重連相關的操作。

  5. __setSubscribed對應的是一個Python集合,用於保存我們通過訂閱函數訂閱過的合約,在斷線重連后自動進行訂閱,之所以選擇set而不是list是為了保證合約的唯一性,避免重復訂閱(盡管重復訂閱也沒影響)。

  6. 在創建對象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)
...
  1. 通過回調函數收到API的數據推送后,創建不同類型的事件Event對象(來自於事件驅動引擎模塊),在事件對象的數據字典dict_中保存需要具體推送的數據,然后推送到事件驅動引擎中,由其負責處理。

  2. 回調函數收到的數據中,data和error分別對應的是保存主要數據(如行情)和錯誤信息的字典,n是該回調函數對應的請求號(即調用主動函數時的reqid),last是一個布爾值,代表是否為該次調用的最后返回信息。

  3. 我們主要對data字典感興趣,因此選擇在事件中整體推送。

  4. 而error字典每次收到后應當立即檢查是否包含錯誤信息(因為即使沒有發生錯誤也會推送),若有則自動保存為一個日志事件(通過日志監控控件顯示出來)。

  5. 服務器連接完成后(onFrontConnected),檢查是否已經填入了用戶名等登錄信息,若有則自動登錄(請參考后面主動函數中的示例)。

  6. 登陸完成后(onRspUserLogin),自動訂閱__setSubscribed中之前已經訂閱過的合約。

  7. 收到行情推送后(onRtnDepthMarketData),我們選擇創建兩種事件,一種是常規行情事件(通常適用於市場行情監控GUI等對所有行情推送都關注的組件),另一種是特定合約行情事件(通常適用於算法等僅關注特定合約行情的組件)。

  8. 當我們調用會有返回信息的主動函數時,需要傳入本次請求的編號,此時我們先將__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)
  1. 主動函數僅封裝了兩個功能登錄login和訂閱合約subscribe。這里假設通常我們不會做登出(直接殺進程)和退訂合約(不一定)之類的操作,有需求的話可以自行封裝對應的函數。

  2. 對於登錄函數login而言,傳入4個參數包括服務器前置機地址address,用戶名userid,密碼password以及經紀商代碼brokerid。函數調用后,我們先將userid,password和brokerid保存下來,然后注冊服務器地址registerFront,並初始化連接init。連接完成后,onFrontConnected回調函數會被自動調用,然后發生的操作請參考前一段落的回調函數工作流程

  3. LTS的API在訂閱行情時,需要傳入合約的代碼以及合約所在的交易所(因為存在兩個證券交易所相同代碼的情況),而CTP的API在期貨方面則不存在該問題,只需傳入合約代碼。發送訂閱請求后,將該訂閱請求保存在__setSubscribed集合中,使得斷線重連時可以自動重新訂閱。

三、總結

  在交易程序的開發中,所有的API對接原理均大同小異,除了類CTP API以外,國內的恆生接口、FIX引擎接口等等也可以同樣遵照以上的原理進行對接設計。

  文章中的例子是行情接口,交易接口因為包含了更多的回調函數和主動函數,在設計上相對更為復雜,感興趣讀者建議直接閱讀demo中的源代碼,相關問題可以在vn.py框架交流群(群號:262656087)中提問。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM