Python量化交易平臺開發教程系列1-類CTP交易API的工作原理
原創文章,轉載請註明出處:用Python的交易員
類CTP交易API簡介
國內程式化交易技術的爆發式發展幾乎就是起源於上期技術公司基於CTP櫃檯推出了交易API,使得使用者可以隨意開發自己的交易軟體直接連線到交易櫃檯上進行交易,同時CTP API的設計模式也成為了許多其他櫃檯上交易API的設計標準,本人已知的類CTP交易API包括:
-
上期CTP
-
飛馬
-
華寶證券LTS
-
飛創Xspeed
-
金仕達
-
恆生UFT
所以這個教程系列選擇從類CTP交易API中的LTS API開始來介紹API的Python封裝方法,真正掌握了以後想要做其他型別API(比如恆生的T2)的封裝也只是大同小異而已。
LTS API檔案說明
通常當用戶從網上下載API的壓縮包,解壓後會看到以下的檔案:
- .h檔案:C++的標頭檔案,包含了API的內部結構資訊,開發C++程式時需要包含在專案內
- .dll檔案:windows下的動態連結庫檔案,API的實體,開發C++程式編譯和連結時用,使用開發好的程式時也必須放在程式的資料夾內
- .lib檔案:windows下的庫檔案,編譯和連結時用,程式開發好後無需放在程式的資料夾內
- .so檔案:linux下的動態連結庫檔案,其他同.dll檔案
.h標頭檔案介紹
.dll、.lib、.so檔案都是編譯好的二進位制檔案,無法開啟,所以從使用者角度我們只需關注.h檔案中的內容。對於不同的API而言,.h檔案的字首可能有所區別,如LTS是SecurityFtdc,CTP是ThostFtdc,下面分別介紹這4個.h檔案。
ApiDataType.h
該檔案中包含了對API中用到的常量的定義,如以下程式碼定義了一個產品型別常量對應的字元:
#define SECURITY_FTDC_PC_Futures '1'
以及型別的定義,如以下程式碼定義了產品名稱型別是一個長度為21個字元的字串:
typedef char TSecurityFtdcProductNameType[21];
ApiStruct.h
該檔案中包含了API中用到的結構體的定義,如以下程式碼定義了交易所這個結構體的構成:
///交易所 struct CSecurityFtdcExchangeField { ///交易所程式碼TSecurityFtdcExchangeIDType ExchangeID; ///交易所名稱 TSecurityFtdcExchangeNameType ExchangeName; ///交易所屬性 TSecurityFtdcExchangePropertyType ExchangeProperty; };
例如TSecurityFtdcExchangeIDType這個型別的定義,可以在ApiDataType.h中找到。
MdApi.h
該檔案中包含了API中的行情相關元件的定義,檔案通常開頭會有一段這樣的內容:
#if !defined(SECURITY_FTDCMDAPI_H) #define SECURITY_FTDCMDAPI_H #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #include "SecurityFtdcUserApiStruct.h" #if defined(ISLIB) && defined(WIN32) #ifdef LIB_MD_API_EXPORT #define MD_API_EXPORT __declspec(dllexport) #else #define MD_API_EXPORT __declspec(dllimport) #endif #else #define MD_API_EXPORT #endif
這些內容主要是一些和作業系統、編譯環境相關的定義,一般使用者忽略就好(作者其實也不太懂...)。
然後是兩個類CSecurityFtdcMdSpi和CSecurityFtdcMdApi的定義。
CSecurityFtdcMdSpi
MdSpi類中包含了行情功能相關的回撥函式介面,什麼是回撥函式呢?簡單來說就是由於櫃檯端向用戶端傳送資訊後才會被系統自動呼叫的函式(非使用者主動呼叫),對應的主動函式會在下面介紹。CSecurityFtdcMdSpi大概看起來是這麼個樣子:
class CSecurityFtdcMdSpi { public: ...... ///登入請求響應 virtual void OnRspUserLogin(CSecurityFtdcRspUserLoginField *pRspUserLogin, CSecurityFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast) {}; ...... ///深度行情通知 virtual void OnRtnDepthMarketData(CSecurityFtdcDepthMarketDataField *pDepthMarketData) {}; };
......省略了部分程式碼。從上面的程式碼中可以注意到:
-
回撥函式都是以On開頭。
-
櫃檯端向用戶端傳送的資訊經過API處理後,傳給我們的是一個結構體的指標,如CSecurityFtdcRspUserLoginField *pRspUserLogin,這裡的pRspUserLogin就是一個C++的指標型別,其指向的結構體物件是CSecurityFtdcRspUserLoginField結構的,而該結構的定義可以在ApiStruct.h中找到。
-
不同的回撥函式,傳過來的引數數量是不同的,OnRspUserLogin中傳入的引數包括兩個結構體指標,以及一個整數(代表該響應對應的使用者請求號)和一個布林值(該響應是否是這個請求號的最後一次響應)。
CSecurityFtdcMdApi
MdApi類中包含了行情功能相關的主動函式結構,顧名思義,主動函式指的是由使用者負責進行呼叫的函式,用於向櫃檯端傳送各種請求和指令,大概樣子如下:
class MD_API_EXPORT CSecurityFtdcMdApi { public: ///建立MdApi ///@param pszFlowPath 存貯訂閱資訊檔案的目錄,預設為當前目錄 ///@return 創建出的UserApi ///modify for udp marketdata static CSecurityFtdcMdApi *CreateFtdcMdApi(const char *pszFlowPath = ""); ...... ///註冊回撥介面 ///@param pSpi 派生自回撥介面類的例項 virtual void RegisterSpi(CSecurityFtdcMdSpi *pSpi) = 0; ///訂閱行情。 ///@param ppInstrumentID 合約ID ///@param nCount 要訂閱/退訂行情的合約個數 ///@remark virtual int SubscribeMarketData(char *ppInstrumentID[], int nCount, char* pExchageID) = 0; ...... ///使用者登入請求 virtual int ReqUserLogin(CSecurityFtdcReqUserLoginField *pReqUserLoginField, int nRequestID) = 0; ...... };
以上程式碼中,需要注意的重點包括:
-
MdApi物件不應該直接建立,而應該通過呼叫類的靜態方法CreateFtdcMdApi建立,傳入引數為你希望儲存API的通訊用的.con檔案的目錄(可以選擇留空,則.con檔案會被放在程式所在的資料夾下)。
-
建立MdSpi物件後,需要使用MdApi物件的RegisterSpi方法將該MdSpi物件的指標註冊到MdApi上,也就是告訴MdApi從櫃檯端收到資料後應該通過哪個物件的回撥函式推送給使用者。從API的這個設計上作者猜測MdApi中後包含了和櫃檯端通訊、接收和傳送資料包的功能,而MdSpi僅僅是用來實現一個通過回撥函式向用戶程式推送資料的介面。
-
絕大部分主動函式(以Req開頭)在呼叫時都會用到一個整數型別的引數nRequestID,該引數在整個API的呼叫中應當保持遞增唯一性,從而在收到回撥函式推送的資料時,可以知道是由哪次操作引起的。
TraderApi.h
該檔案中包含了API中的交易相關元件的定義,檔案同樣以一段看不懂的定義開頭,然後包含了兩個類CSecurityFtdcTraderSpi和CSecurityFtdcTraderApi,這兩個類和MdApi中的兩個類在結構上非常接近,區別僅僅在於類包含的方法函式上。
CSecurityFtdcTraderSpi
class CSecurityFtdcTraderSpi { public: ///當客戶端與交易後臺建立起通訊連線時(還未登入前),該方法被呼叫。 virtual void OnFrontConnected(){}; ... ///錯誤應答 virtual void OnRspError(CSecurityFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast) {}; ///登入請求響應 virtual void OnRspUserLogin(CSecurityFtdcRspUserLoginField *pRspUserLogin, CSecurityFtdcRspInfoField *pRspInfo, int nRequestID, bool bIsLast) {}; ... ///報單通知 virtual void OnRtnOrder(CSecurityFtdcOrderField *pOrder) {}; ... ///報單錄入錯誤回報 virtual void OnErrRtnOrderInsert(CSecurityFtdcInputOrderField *pInputOrder, CSecurityFtdcRspInfoField *pRspInfo) {}; ... };
Spi(包括MdSpi和TraderSpi)類的回撥函式基本上可以分為以下四種:
-
以On...開頭,這種回撥函式通常是返回API連線相關的資訊內容,與業務邏輯無關,返回值(即回撥函式的引數)通常為空或是簡單的整數型別。
-
以OnRsp...開頭,這種回撥函式通常是針對使用者的某次特定業務邏輯操作返回資訊內容,返回值通常會包括4個引數:業務邏輯相關結構體的指標,錯誤資訊結構體的指標,本次操作的請求號整數,是否是本次操作最後返回資訊的布林值。其中OnRspError主要用於一些通用錯誤資訊的返回,因此返回的值中不包含業務邏輯相關結構體指標,只有3個返回值。
-
以OnRtn...開頭,這種回撥函式返回的通常是由櫃檯向用戶主動推送的資訊內容,如客戶報單狀態的變化、成交情況的變化、市場行情等等,因此返回值通常只有1個引數,為推送資訊內容結構體的指標。
-
以OnErrRtn...開頭,這種回撥函式通常由於使用者進行的某種業務邏輯操作請求(掛單、撤單等等)在交易所端觸發了錯誤,如使用者發出撤單指令、但是該訂單在交易所端已經成交,返回值通常是2個引數,即業務邏輯相關結構體的指標和錯誤資訊的指標。
CSecurityFtdcTraderApi
class TRADER_API_EXPORT CSecurityFtdcTraderApi { public: ///建立TraderApi ///@param pszFlowPath 存貯訂閱資訊檔案的目錄,預設為當前目錄 ///@return 創建出的UserApi static CSecurityFtdcTraderApi *CreateFtdcTraderApi(const char *pszFlowPath = ""); ... ///初始化 ///@remark 初始化執行環境,只有呼叫後,接口才開始工作 virtual void Init() = 0; ... ///使用者登入請求 virtual int ReqUserLogin(CSecurityFtdcReqUserLoginField *pReqUserLoginField, int nRequestID) = 0; ... };
Api類包括的主動函式通常分為以下三種: 1. Create...,類的靜態方法,用於建立API物件,傳入引數是用來儲存API通訊.con檔案的資料夾路徑。
-
Req...開頭的函式,可以由使用者主動呼叫的業務邏輯請求,傳入引數通常包括2個:業務請求結構體指標和一個請求號的整數。
-
其他非Req...開頭的函式,包括初始化、訂閱資料流等等引數較為簡單的功能,傳入引數的數量和型別視乎函式功能不一定。
API工作流程
簡單介紹一下MdApi和TraderApi的一般工作流程,這裡不會包含太多細節,僅僅是讓讀者有一個概念。
MdApi
-
建立MdSpi物件
-
呼叫MdApi類以Create開頭的靜態方法,建立MdApi物件
-
呼叫MdApi物件的RegisterSpi方法註冊MdSpi物件的指標
-
呼叫MdApi物件的RegisterFront方法註冊行情櫃檯的前置機地址
-
呼叫MdApi物件的Init方法初始化到前置機的連線,連線成功後會通過MdSpi物件的OnFrontConnected回撥函式通知使用者
-
等待連線成功的通知後,可以呼叫MdApi的ReqUserLogin方法登陸,登陸成功後會通過MdSpi物件的OnRspUserLogin通知使用者
-
登陸成功後就可以開始訂閱合約了,使用MdApi物件的SubscribeMarketData方法,傳入引數為想要訂閱的合約的程式碼
-
訂閱成功後,當合約有新的行情時,會通過MdApi的OnRtnDepthMarketData回撥函式通知使用者
-
使用者的某次請求發生錯誤時,會通過OnRspError通知使用者。
-
MdApi同樣提供了退訂合約、登出的功能,一般退出程式時就直接殺程序(不太安全)
TraderApi
-
TraderApi和MdApi類似,以下僅僅介紹不同點
-
註冊TraderSpi物件的指標後,需要呼叫TraderApi物件的SubscribePrivateTopic和SubscribePublicTopic方法去選擇公開和私有資料流的重傳方法(這一步MdApi沒有)
-
對於期貨櫃臺而言(CTP、恆生UFT期貨等),在每日第一次登陸成功後需要先查詢前一日的結算單,等待結算單查詢結果返回後,確認結算單,才可以進行後面的操作;而證券櫃檯LTS無此要求
-
上一步完成後,使用者可以呼叫ReqQryInstrument的方法查詢櫃檯上所有可以交易的合約資訊(包括程式碼、中文名、漲跌停、最小价位變動、合約乘數等大量細節),一般是在這裡獲得合約資訊列表後,再去MdApi中訂閱合約;經常有人問為什麼在MdApi中找不到查詢可供訂閱的合約程式碼的函式,這裡尤其要注意,必須通過TraderApi來獲取
-
當用戶的報單、成交狀態發生變化時,TraderApi會自動通過OnRtnOrder、OnRtnTrade通知使用者,無需額外訂閱
總結
第一篇教程到這裡已經接近結束了,如果你是一個沒有任何交易API開發經驗的讀者,並且堅持看了下來,此時你心中很可能有這麼個想法:我X,API開發這麼複雜???!!!
相信我,這是人之常情(某些讀者如果覺得很好理解那作者真是佩服你了),作者剛開始的時候大概在CTP API的標頭檔案和網上的教程資料、示例中糾結了3個多月而不得入門,當時也沒有任何C++的開發經驗(我是金融工程出生,大學裡程式設計只學了VBA和Matlab,還幾乎都是些演算法方面的內容),邊學語言邊研究怎麼開發,真心痛苦。
在這裡,我想告訴讀者的一個好訊息是:還剩兩篇教程,我們基本就可以和C++ say goodbye,進入Python靈活快速開發的世界了。同時對於絕大部分不打算自己去封裝API的讀者,這三篇文章可以走馬觀花的過一遍,不會影響任何你未來對於vn.py框架的使用。
當然,對於有恆心和毅力的讀者,100%自己掌握API的封裝技術是一項絕對值得投入時間和精力的事情。在很多人的觀念中Python並不適合用來開發低延遲的交易平臺,這裡作者可以用親身經驗告訴你:那只是在純用Python的情況下。作為一門膠水語言,Python最大的特點之一就是易於通過混合程式設計來進行拓展,使用者可以在真正需要優化的地方進行最深度的定製優化,把自己有限的時間、精力花在刀刃上。在交易API層面,可以定製的地方包括C++層面的資料結構改變、資料預處理、回撥函式傳遞順序調整等等諸多的優化,這些只有在你完全掌握API的封裝後才能辦得到