【前言】對上海期貨交易平台CTP接口的一個學習總結.(參考vn.py官方文檔)
一、引言
目前本人所在的公司一共有三款平台,分別基於C++, C#和Python。其中C#和Python平台都是由交易員開發;C++平台則是由專職IT團隊作為一個通用平台開發,內部組件進行了封裝(交易員不可見),對外提供行情、交易的API用於策略開發(除了C++ 外也包括C#和Python可用的API)。
用C++ 開發的交易系統:
理論上這款C++平台應該是最為穩定和強大的,由專業人士設計,同時采用封裝核心,暴露API,支持組件模塊開發,linux服務器運行的形式。
但是在實際運用中,交易團隊表達了一個強烈的觀點:這個平台實在是太難用了!
-
由IT團隊設計的API功能非常強大,但是也太過繁瑣,導致學習曲線極為陡峭。
-
為了追求速度,沒有設計原生GUI(本來就為了在Linux服務器上跑),但是今天絕大多數的非超高頻(追求微秒級延遲的那種)交易策略,幾乎都需要有人實時監控,你總不能讓交易員盯着個linux shell上不斷print出來的內容或者盤中去翻日志吧,這個運維風險就扛不起。盡管可以作為插件的形式開發GUI,但C++本身的GUI開發還是較為復雜的,非專業IT很難搞的定。
-
交易員團隊的需求變化很快,通常等不及IT去排班開發,最好是今天收盤有個點子,明天開盤就能開始接實盤數據驗證,沒問題后天就能上實盤。比如去年四季度的分級基金套利機會就是稍縱即逝,那段時間如果能快速開發完成一套專門的監控套利系統,抓住的利潤絕對會比用excel接wind數據來的多不少。
-
某些業務邏輯確實太過復雜,交易員想解釋讓IT明白,無奈IT並不是太擅長某些金融領域(比如期權高頻套利的整個業務框架),交流成本太高。
用web開發來做比較的話,C++實現的量化交易平台像是java在網絡開發領域的地位,強大(幾乎無所不能)、穩定(無數大公司的支持),但是也很臃腫(你一兩個人開發試試)。
用python來開發的交易系統:
優點:
-
動態語言的快速開發特性,封接口有boost.Python,寫GUI有PyQt,時間序列有numpy,等等,幾乎你想干的事都有現成的庫可以用;
-
學習成本低,這點算是個共識了吧?
-
真需要低延遲的時候,膠水語言很容易通過其他語言拓展:cython, ctypes, boost.Python等等。
-
運行速度足夠快,也許和C++比起來確實慢了不少,但是就我的經驗來看,這點速度延遲對90%的策略pnl毫無影響。
缺點:
-
GIL,該死的全局鎖導致Python無法有效利用多核CPU的性能,盡管可以通過拓展繞過去,但還是沒有其他語言原生多線程利用多核的方案來的簡便。
-
沒有靜態類型檢查,重構的時候確實有點痛苦,不過一個良好的編程范式可以有效解決這個問題。
-
不適合用來搞超高頻策略(追求微秒級延遲的差異),得承認這點Python確實搞不過C++。常規基於TICK級數據的策略沒問題。
二、CTP交易API的工作原理
linux64位下的接口文件

- .h文件:C++的頭文件,包含了API的內部結構信息,開發C++程序時需要包含在項目內;
- .so文件:linux下的動態鏈接庫文件,其他同.dll文件
- .dll文件:windows下的動態鏈接庫文件,API的實體,開發C++程序編譯和鏈接時用,使用開發好的程序時也必須放在程序的文件夾內;
- .lib文件:windows下的庫文件,編譯和鏈接時用,程序開發好后無需放在程序的文件夾內;
后面兩個是windows下的庫,linux下木有。本教程下面所有的Security可換成thost.
1、.h頭文件介紹
四個.h頭文件,數據類型的定義、api結構體定義、行情API和交易API。
(1)apidatatype.h
該文件中包含了對API中用到的常量的定義,如以下代碼定義了一個產品類型常量對應的字符:
#define SECURITY_FTDC_PC_Futures '1'
以及類型的定義,如以下代碼定義了產品名稱類型是一個長度為21個字符的字符串:
typedef char TSecurityFtdcProductNameType[21];
(2)apistruct.h
該文件中包含了API中用到的結構體的定義,如以下代碼定義了交易所這個結構體的構成:
///交易所
struct CSecurityFtdcExchangeField
{
///交易所代碼
TSecurityFtdcExchangeIDType ExchangeID;
///交易所名稱
TSecurityFtdcExchangeNameType ExchangeName;
///交易所屬性
TSecurityFtdcExchangePropertyType ExchangeProperty;
};
(3)MdApi.h
該文件中包含了API中的行情相關組件的定義,文件通常開頭會有一段這樣的內容:
#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
這些內容主要是一些和操作系統、編譯環境相關的定義,一般用戶忽略就好(作者其實也不太懂...)。
然后是兩個類CThostFtdcMdSpi和CThostFtdcMdApi的定義。
<1>CThostFtdcMdSpi
MdSpi類中包含了行情功能相關的回調函數接口,什么是回調函數呢?簡單來說就是由於櫃台端向用戶端發送信息后才會被系統自動調用的函數(非用戶主動調用),對應的主動函數會在下面介紹。CThostFtdcMdSpi大概看起來是這么個樣子:
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中傳入的參數包括兩個結構體指針,以及一個整數(代表該響應對應的用戶請求號)和一個布爾值(該響應是否是這個請求號的最后一次響應)。
<2>CThostFtdcMdApi
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的調用中應當保持遞增唯一性,從而在收到回調函數推送的數據時,可以知道是由哪次操作引起的。
(4)TraderApi.h
該文件中包含了API中的交易相關組件的定義,文件同樣以一段看不懂的定義開頭,然后包含了兩個類CThostFtdcTraderSpi和CThostFtdcTraderApi,這兩個類和MdApi中的兩個類在結構上非常接近,區別僅僅在於類包含的方法函數上。
<1>FtdcTraderSpi
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個參數,即業務邏輯相關結構體的指針和錯誤信息的指針。
<2>CThostFtdcTraderApi
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...開頭的函數,包括初始化、訂閱數據流等等參數較為簡單的功能,傳入參數的數量和類型視乎函數功能不一定。
2、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的封裝后才能辦得到。
