IB API - twkk/cta_strategy GitHub Wiki

作者:吳鹹樾

目錄

背景介紹

IB API能成為許多量化平台的首選對接通道,不外乎一個原因:Created by traders, for traders。 公司創始人Thomas Peterffy的傳奇背景就不多介紹了,關鍵在於IB背後的高頻交易公司TimberHill,有這麽一家市場上頂尖的玩家在為公司經紀相關的業務提供建議和各種技術支持,IB才可能同時給客戶提供:

  • 提供強到變態的交易平台TWS
  • 多到變態的全球產品覆蓋
  • 低到變態的交易費用

以上三點能做得比IB好的不能說沒有,但是三點同時都能和IB競爭的似乎就不多見了:

  • 高盛之類的投行太過高大上,不夠親民
  • E*Trade之類的零售經紀商,費用較貴的同時,交易軟件的功能還十分有限
  • FXCM之類的外匯公司,經常留點頭寸不對沖跟客戶對賭,點差還特別高

所以綜合來看,其實IB成功的原因和其他行業成功的公司也沒多大區別,能夠抓住客戶的痛點並且長期提供高質來那個的服務才是關鍵。

API的特點

  1. 使用IB API連接上的不是IB的交易服務器,而是運行於本機的TWS(帶圖形的交易平台)或者IB Gateway(只提供簡單日志的交易路由,適合量化),即使對於FIX接口也是如此。如下圖所示: IB API

  2. IB API提供了所有的底層源代碼,用戶在使用時需要自行編譯底層API接口部分的組件,提供了一定靈活性的同時,也導致項目開發過程中容易遇到額外的編譯問題。

  3. IB API內建了一個比較大的內存緩沖區,回調函數即使阻塞一段時間也基本不會導致API崩潰,所以CTP類API開發時常用的一個回調函數推送的數據必須先進緩沖隊列的設計模式就沒有必要了,可以把一些可能耗時較長的邏輯直接寫在回調函數中(出於程序代碼架構的原因仍然不建議這麽做,不過對於API封裝來說省了很大的力氣)。

  4. IB API裏回調函數(負責向用戶的程序中推送數據)的工作線程需要用戶自行創建和管理(國內幾乎所有的API都是內部實現的),提供了極大的靈活性(比如用戶可以使用類似協程的模式輕松實現一個線程同時管理8個API連接的推送),但是由於官方文檔和Demo的缺失導致用戶上手困難。

基本配置

TWS & IB網關

The Trader Workstation (TWS)是IB提供的交易平台軟件,它支持全世界100多個市場的股票,期權,期貨,外匯,債券,基金的交易,功能十分強大。而IB網關作為TWS的替代品更為小巧,但是從TWS API的角度看,兩者並無差別:都是作為建立Socket連接的服務器。由於IB網關沒有覆雜的圖形界面,能夠提供比TWS更高的交易性能。

啟用API連接

在用vnpy連接IB接口前,需要在TWS或IB網關上進行配置,選擇菜單 Edit -> Global Configuration -> API -> Settings 確保 "Enable ActiveX and Socket Clients" 被勾選,如下圖所示: 激活Socket連接

IB API的基本結構

TWS API 包含以下幾個重要組成成分:

  • EWrapper:類似CTP中的SPI類,提供回調函數,TWS通過它將信息傳給API客戶端應用。通過實現這個接口,客戶端應用才能接收和處理TWS發來的信息。
  • EClientSocket:類似CTP中的API類,提供主動函數。其對象用作向TWS發送請求。EClientSocket類需要實現Ewrapper接口作為其構造參數的一部份。

下圖以獲取行情數據為例,簡單介紹EClientSocket和EWrapper工作原理: 基本流程

reqMktData()是請求獲取市場行情數據的函數,EClientSocket類的方法,即主動函數。tickPrice()為Ewrapper類的方法,需要用戶實現,用於接收市場行情數據。當調用reqMktData時,EClientSocket對象將會創造一個線程,從socket讀取數據並觸發Ewrapper類裏的回調函數。

IB API 源代碼

IB提供官方API開發的SDK,下載地址。直接下載穩定版。裏面包含C++,Java和C#的代碼和示例。下圖為C++ API源文件: 源代碼

其中:

  • EClient.cpp和EClient.h:定義EClient類,包含所以EClient類的主動函數;
  • DefaultEWrapper.cpp,DefaultEWrapper.h和EWrapper.cpp:聲明EWrapper類的回調函數,具體需要用戶自己實現,VNPY中實現了部分函數;
  • EClientSocket.cpp和EClientSocket.h:定義EClient的Socket通信;
  • EDecoder.cpp和EDecoder.h:定義響應處理函數,即EClient類方法與EWrapper類方法的映射關系;
  • EReaderOSSignal.cpp和EReaderOSSignal.h:定義一個信號量類和對應的方法,回調函數管理線程需要監聽該信號量,當socket收到數據後該信號量會被觸發;
  • EReader.cpp和EReader.h:當上面的信號量被觸發後,用戶需要調用EReader中的處理信息函數,來觸發EWrapper中對應的回調函數;
  • Contract.h:定義合約類;
  • Order.h:定義訂單類;

IB API 工作流程

以下是一個API的大體工作順序:

  1. 繼承EWrapper並實現回調函數
//步驟1

class IbWrapper : public EWrapper
{
private:
	VnIbApi \*api;

public:
	IbWrapper(VnIbApi \*api)
	{
		this->api = api;
	};

	~IbWrapper()
	{

	};

	void tickPrice(TickerId tickerId, TickType field, double price, int canAutoExecute);

......
  1. 創建EWrapper對象wrapper
  2. 創建EReaderSignal對象signal
  3. 創建EClientSocket對象client,傳入wrapper和client的對象指針作為構造參數
//步驟2、3、4

class VnIbApi
{
private:
	//EClientSocket \*client;
	IbWrapper \*wrapper;
	EReaderOSSignal signal;
	EReader \*reader;

	thread \*worker;

public:
	EClientSocket \*client;

	VnIbApi()
	{
		this->signal = EReaderOSSignal();
		this->wrapper = new IbWrapper(this);
		this->client = new EClientSocket(this->wrapper, &this->signal);
	};

......
  1. 調用client的eConnect方法,連接TWS程序
  2. 連接成功後,創建EReader對象reader,傳入client和signal的對象指針作為構造函數
  3. 調用reader的start方法,啟動reader中的socket端口數據監聽線程
//步驟5、6、7
bool VnIbApi::eConnect(string host, int port, int clientId, bool extraAuth)
{
	bool r = this->client->eConnect(host.c_str(), port, clientId, extraAuth);

	if (r)
	{
		//啟動EReader從socket讀取信息
		this->reader = new EReader(this->client, &this->signal);
		this->reader->start();

		//啟動數據推送線程
		function0<void> f = boost::bind(&VnIbApi::run, this);
		thread t(f);
		this->worker = &t;
	};

	return r;
};
  1. 啟動回調函數管理線程,進入無限循環,首先調用signal的waitForSignal等待信號的觸發
  2. socket收到數據後,信號被觸發,調用reader的processMsgs函數,激發wrapper中對應的回調函數
//步驟8、9

void VnIbApi::run()
{
	while (this->client->isConnected())
	{
		this->reader->checkClient();
		signal.waitForSignal();
		this->reader->processMsgs();
	}
};

建立連接

通過IBApi.EClientSocket.eConnect函數可以在API客戶端應用與TWS間建立TCP連接。TWS可被看作是一個服務器主動地監聽發向給定端口的連接請求。對於一個客戶ID,TWS可以同時最多建立32個客戶端應用。

在默認情況下,每次建立連接都需要在TWS窗口中確認,TWS支持自動接受信任IP地址或本地連接,設置方法如下圖: 自動連接設置

當Soket連接建立完成後,客戶端和TWS就可以開始交換信息。API連接支持異步和同步兩種連接請求。如果采用異步方式,客戶端需要收到TWS的確認後才能嘗試發送信息。

合約

IBApi.Contract對象可表示所有交易工具。每當一個需要合約信息的請求(如獲取市場行情,下單等)發送給TWS,系統會自動匹配一個唯一的合約。如果不能找到唯一的合約,TWS會返回錯誤信息。

Contract Details獲取

在進行行情訂閱或下單之前,需要獲取合約的細節信息。這需要調用主動函數IBApi.EClient.reqContractDetails,之後與請求合約參數對應的,包含所有信息的完整合約對象將被返回到IBApi::EWrapper::contractDetails函數中。但是債券合約需要用IBApi::EWrapper::bondContractDetails函數接收。

基本合約

  • 外匯

    Contract contract;
    contract.symbol = "EUR";
    contract.secType = "CASH";
    contract.currency = "GBP";
    contract.exchange = "IDEALPRO";
  • 股票

    Contract contract;
    contract.symbol = "IBKR";
    contract.secType = "STK";
    contract.currency = "USD";
    //In the API side, NASDAQ is always defined as ISLAND
    contract.exchange = "ISLAND";

    對於某些Smart-Routed股票合約,可能會有相同的股票代碼,貨幣和交易所,這時需要特別給出primary exchange字段的信息,具體如下:

    Contract contract;
    contract.symbol = "MSFT";
    contract.secType = "STK";
    contract.currency = "USD";
    contract.exchange = "SMART";
    //Specify the Primary Exchange attribute to avoid contract ambiguity
    contract.primaryExchange = "ISLAND";

    或者直接在exchange字段中寫入“Exchange:PrimaryExchange”的形式,比如,“SMART:NASDAQ”。

  • 指數

    Contract contract;
    contract.symbol = "DAX";
    contract.secType = "IND";
    contract.currency = "EUR";
    contract.exchange = "DTB";
  • 期貨

    期貨合約需要給出到期日期和標的物的代碼。

    Contract contract;
    contract.symbol = "ES";
    contract.secType = "FUT";
    contract.exchange = "GLOBEX";
    contract.currency = "USD";
    contract.lastTradeDateOrContractMonth = "201612";
  • 期權

    Contract contract;
    contract.symbol = "GOOG";
    contract.secType = "OPT";
    contract.exchange = "BOX";
    contract.currency = "USD";
    contract.lastTradeDateOrContractMonth = "20170120";
    contract.strike = 615;
    contract.right = "C";
    contract.multiplier = "100";

訂單

IB API支持市價單,限價單等近30種基本訂單和對沖、條件單和算法下單等高級訂單。

訂單管理

  • 下單

在下單之前,需要獲取下一個有效識別號(the Next Valid Identifier)。它需要調用主動函數IBApi.EClient.reqIds,並返回給 IBApi.EWrapper.nextValidId。當只有一個客戶端連接到TWS時,不需要每次下單都獲取下一個有效識別號,而是在之前號碼上加一即可,但是如果有多個客戶端連接到同一個TWS帳戶,則不能這樣。

在獲得下一個有效識別號後,就可以調用IBApi.EClient.placeOrder方法提交訂單。當訂單提交成功後,TWS會通過 IBApi.EWrapper.openOrder和IBApi.EWrapper.orderStatus兩個函數發送訂單狀態。

另外,可以將IBApi.Order.WhatIf標識設置成True獲取傭金和保證金信息。這種方式不會真實下單,而是發起一個查詢請求。傭金和保證金信息會通過IBApi.EWrapper.openOrder回調的IBApi.OrderState對象返回。

  • 修改訂單

修改訂單只需要重新調用IBApi.Eclient.placeOrder函數,除了需要改變的參數外,其他的參數都設置成和之前的訂單一致。其中IBApi.Order.OrderId必須和之前一致。原則上不推薦修改訂單價格和數量之外的參數,如果需要修改其他參數,最好先撤單再重新下單。

  • 撤銷訂單

撤銷訂單可調用IBApi::EClient::cancelOrder和IBApi::EClient::reqGlobalCancel。cancelOrder只能撤銷由同一個ID的客戶端提交的訂單,且只需要給出原始訂單ID。IBApi::EClient::reqGlobalCancel會撤銷所有未成交訂單。

  • 獲取未成交訂單

調用IBApi.EClient.reqOpenOrders函數可獲取當前客戶端ID提交的未成交訂單。而IBApi.EClient.reqAllOpenOrders函數能獲取所有未成交的訂單,不管是否是當前客戶端提交的。這些訂單的信息通過回調函數IBApi.EWrapper.openOrder和IBApi.EWrapper.openStatus返回。

  • 成交和傭金

當一個訂單全部或部分成交後,IBApi.EWrapper.execDetails和IBApi.EWrapper.commissionReport事件會傳遞出IBApi.Execution和IBApi.CommissionReport兩個對象,裏面包括所有的訂單成交和傭金信息。如果要獲取所有客戶端的傭金回報,需要以Master Client ID進行連接。

class TestCppClient : public EWrapper //回調函數
{
...
void TestCppClient::execDetails( int reqId, const Contract& contract, const Execution& execution) {
    printf( "ExecDetails. ReqId: %d - %s, %s, %s - %s, %ld, %g\n", reqId, contract.symbol.c_str(), contract.secType.c_str(), contract.currency.c_str(), execution.execId.c_str(), execution.orderId, execution.shares);
}
...
void TestCppClient::commissionReport( const CommissionReport& commissionReport) {
    printf( "CommissionReport. %s - %g %s RPNL %g\n", commissionReport.execId.c_str(), commissionReport.commission, commissionReport.currency.c_str(), commissionReport.realizedPNL);
}

如果需要,可以調用IBApi.EClient.reqExecutions方法獲取成交和傭金信息。IBApi.ExecutionFilter 需要作為reqExecutions的輸入參數,提供訂單匹配標準。當所有的成交訂單信息都發送完畢,IBApi.EWrapper.execDetailsEnd將會被觸發。

class TestCppClient : public EWrapper
{
...
void TestCppClient::execDetailsEnd( int reqId) {
    printf( "ExecDetailsEnd. %d\n", reqId);
}

行情

用IB API可以獲取以下幾種行情數據:

  1. Top Market Data (Level I)
  2. Market Depth (Level II)
  3. Historical Data
  4. Real Time Bars

為了獲得以上市場數據,需要在TWS中訂閱相應交易品種的實時行情。需滿足一下要求:

  1. 對應品種的交易權限
  2. 已入金的賬戶
  3. 對應一個賬戶的行情訂閱

當用戶請求某個品種的行情數據時,TWS將發起一個Market Data Line. Market Data Line代表活躍的用戶行情數據請求。在默認情況下,一個用戶最多能擁有100個Market Data Lines,即同時獲取有100個品種的行情數據。

Top Market Data (Level I)

首先,IB API給出的行情數據並不是真實的tick-by-tick的價格變動,而是按某個頻率的市場快照。不同的品種會略有不同:

品種 頻率
默認 250 ms
美國期權 100 ms
外匯 5 ms
  • 行情數據請求

為獲取實時報價,需要調用IBApi.EClient.reqMktData函數。

m_pClient->reqMktData(1001, ContractSamples::StockComboContract(), "", false, false, TagValueListSPtr());
m_pClient->reqMktData(1002, ContractSamples::OptionWithLoacalSymbol(), "", false, false, TagValueListSPtr());

如需要歷史波動率等額外信息,可以運用Generic Tick Types,即在reqMktData的輸入參數genericTickList字段加入對應編碼。

  • 接收行情數據

當訂閱好Ticker流後,API客戶端需要通過對應的回調函數來接收TWS發來的行情數據。其中最常用的兩個函數是IBApi.EWrapper.tickPrice和IBApi.EWrapper.tickSize。當它們的返回結果是-1時,說明當前數據無法獲取,通常是由於市場休市,也可能是由於交易不頻繁的合約正好在請求的時刻沒有行情數據。

  • 撤銷行情訂閱

撤銷行情訂閱需要調用IBApi::EClient::cancelMktData函數,該函數需要的唯一參數就是之前訂閱請求的ID號。

Market Depth (Level II)

市場深度數據通過調用IBApi.EClient.reqMarketDepth函數獲取,其必須指定特定的交易所(direct-routed),而不能像Level I數據一樣運用smart-routed的報價。

 m_pClient->reqMktDepth(2001, ContractSamples::EurGbpFx(), 5, TagValueListSPtr());

市場深度數據將通過IBApi.EWrapper.updateMktDepth和 IBApi.EWrapper.updateMktDepthL2兩個回調函數獲取。如果某個品種存在多個做市商,市場深度數據將通過updateMktDepthL2函數接收,否則通過updateMktDepth接收。

class TestCppClient : public EWrapper
{
...
void TestCppClient::updateMktDepth(TickerId id, int position, int operation, int side, double price, int size) {
    printf( "UpdateMarketDepth. %ld - Position: %d, Operation: %d, Side: %d, Price: %g, Size: %d\n", id, position, operation, side, price, size);
}
...
void TestCppClient::updateMktDepthL2(TickerId id, int position, std::string marketMaker, int operation, int side, double price, int size) {
    printf( "UpdateMarketDepthL2. %ld - Position: %d, Operation: %d, Side: %d, Price: %g, Size: %d\n", id, position, operation, side, price, size);
}

撤銷市場深度數據訂閱,需調用IBApi.EClient.cancelMktDepth函數。

歷史數據

可以通過IB API從TWS獲取某個合約的歷史數據,但需要用戶訂閱該合約的Level I數據。

獲取歷史數據需要調用IBApi.EClient.reqHistoricalData函數,每個請求需包含:

  1. 唯一的編號,便於接收對應的數據;
  2. 對應的IBApi.Contract;
  3. 請求數據的截至日期和時間;
  4. 數據的時間長度(從截止時間往前推);
  5. 數據的粒度,最短1秒,最長一個月;
  6. 數據類型,包括Bid,Ask,歷史波動率等;
  7. 是否只獲取常規交易時間的數據;
  8. 接收數據的時間格式;
std::time_t rawtime;
std::tm* timeinfo;
char queryTime [80];
std::time(&rawtime);
timeinfo = std::localtime(&rawtime);
std::strftime(queryTime, 80, "%Y%m%d %H:%M:%S", timeinfo);
m_pClient->reqHistoricalData(4001, ContractSamples::EurGbpFx(), queryTime, "1 M", "1 day", "MIDPOINT", 1, 1, TagValueListSPtr());
m_pClient->reqHistoricalData(4002, ContractSamples::EuropeanStock(), queryTime, "10 D", "1 min", "TRADES", 1, 1, TagValueListSPtr());

另外,可以調用IBApi::EClient::reqHeadTimestamp函數獲取對應合約的歷史數據的最早時間。結果將會通過IBApi::Client::headTimestamp返回。

歷史數據將以K線的形式返回給IBApi::EWrapper::historicalData,當所有數據都接收完畢,將會發送IBApi.EWrapper.historicalDataEnd標識。

class TestCppClient : public EWrapper
{
...
void TestCppClient::historicalData(TickerId reqId, const std::string& date, double open, double high, double low, double close, int volume, int barCount, double WAP, int hasGaps) {
    printf( "HistoricalData. ReqId: %ld - Date: %s, Open: %g, High: %g, Low: %g, Close: %g, Volume: %d, Count: %d, WAP: %g, HasGaps: %d\n", reqId, date.c_str(), open, high, low, close, volume, barCount, WAP, hasGaps);
}
...
void TestCppClient::historicalDataEnd(int reqId, std::string startDateStr, std::string endDateStr) {
    std::cout << "HistoricalDataEnd. ReqId: " << reqId << " - Start Date: " << startDateStr << ", End Date: " << endDateStr << std::endl;   
}

Real Time Bars

可用IBApi.EClient.reqRealTimeBars函數請求實時K線數據,不過數據的粒度只能是5秒,即每5秒種發送一次OHLC值。數據接收和訂閱撤銷分別調用IBApi.EWrapper.realtimeBar和IBApi.EClient.cancelRealTimeBars函數。

Python封裝的結構設計

class VnIbApi
{
private:
	IbWrapper \*wrapper;
	EReaderOSSignal signal;
	EReader \*reader;

	thread \*worker;

public:
	EClientSocket \*client;

	VnIbApi()
	{
		this->signal = EReaderOSSignal(2000);
		this->wrapper = new IbWrapper(this);
		this->client = new EClientSocket(this->wrapper, &this->signal);
	};

	~VnIbApi()
	{
		delete this->client;
		delete this->wrapper;
	};

	//-------------------------------------------------------------------------------------
	//負責調用checkMessages的線程工作函數
	//-------------------------------------------------------------------------------------
	void run();

	//-------------------------------------------------------------------------------------
	//回調函數
	//-------------------------------------------------------------------------------------
	virtual void tickPrice(TickerId tickerId, TickType field, double price, int canAutoExecute){};
	...
	//-------------------------------------------------------------------------------------
	//主動函數
	//-------------------------------------------------------------------------------------
	void reqMktData(TickerId id, const Contract& contract,
	const std::string& genericTicks, bool snapshot, const TagValueListSPtr& mktDataOptions);
	...

封裝中的類和函數

  • 封裝後的Python API取名為VnIbApi,另外將原生API中的EWrapper類封裝成IbWrapper,裏面包含所有回調函數的封裝;
  • 由於原生API的EWrapper類中並沒有給出回調函數的實現,所以所有的回調函數都需要用戶實現,當前版本的vnpy只實現了部分功能;
  • IB API封裝後的主動和回調函數名稱與原生函數名一致;

VnIbApi的成員變量

  • wrapper:IbWarpper對象,回調函數類;
  • signal:EReaderOSSignal類對象,回調函數管理線程需要監聽該信號量,當socket收到數據後該信號量會被觸發;
  • reader:EReader類對象,負責從底層的緩沖區讀取數據並推送給用戶;
  • client:EClientSoket類對象,用於調用原生主動函數;
  • worker:一個boost線程指針,用於實現任務線程的工作;

工作步驟

下面以請求市場行情數據為例,簡單介紹Python API工作流程:

  1. 用戶在Python中調用reqMktData函數,傳入參數為包含TickerId,合約對象等信息,該函數自動調用原生API的主動函數並進行參數傳遞;
  2. 回調函數管理線程的socket收到數據後,信號被觸發,調用reader的processMsgs函數,激發wrapper中對應的回調函數;
  3. 封裝後的回調函數被觸發,它負責將原生回調函數中的數據做一層包裝處理,然後再推送到Python環境中;
  4. 具體對數據的操作需要用戶在Python環境中實現;

Python封裝的函數實現

下面仍然以行情獲取為例,說明Python API的函數實現。

構造、析構函數

VnIbApi()
{
	this->signal = EReaderOSSignal(2000);
	this->wrapper = new IbWrapper(this);
	this->client = new EClientSocket(this->wrapper, &this->signal);
};

~VnIbApi()
{
	delete this->client;
	delete this->wrapper;
};

構造函數中會創建創建EWrapper對象wrapper,EReaderSignal對象signal,EClientSocket對象client,傳入wrapper和client的對象指針作為構造參數。而在析構函數中會刪除client和wrapper兩個變量。

TWS連接

bool VnIbApi::eConnect(string host, int port, int clientId, bool extraAuth)
{
	bool r = this->client->eConnect(host.c_str(), port, clientId, extraAuth);

	if (r)
	{
		this->reader = new EReader(this->client, &this->signal);
		this->reader->start();

		function0<void> f = boost::bind(&VnIbApi::run, this);
		thread t(f);
		this->worker = &t;
	};

	return r;
};

當API客戶端完成與TWS的連接後,將會創建EReader對象,以及一個任務處理函數run的工作線程,並將該線程的指針綁定到worker上。

任務處理函數

void VnIbApi::run()
{
	while (this->client->isConnected())
	{
		this->reader->checkClient();
		signal.waitForSignal();
		this->reader->processMsgs();
	}
};

在任務處理函數run中,啟動reader中的socket端口數據監聽線程,然後調用signal的waitForSignal等待信號的觸發,當socket收到數據後,信號被觸發,調用reader的processMsgs函數,激發wrapper中對應的回調函數。processMsg負責從socket層的緩沖區讀取數據,並調用EDecoder執行解碼,然後根據解碼的結果,再調用EWrapper的回調函數。

主動函數

void VnIbApi::reqMktData(TickerId id, const Contract& contract, const std::string& genericTicks, bool snapshot, const TagValueListSPtr& mktDataOptions)
{
	this->client->reqMktData(id, contract, genericTicks, snapshot, mktDataOptions);
};

用戶在Python環境中調用VnIbApi類的reqMkData函數,該函數會直接調用原生API的reqMktData函數,並傳遞參數給原始API函數。由於參數很好的兼容,不需要做格式轉換。

回調函數

void IbWrapper::tickPrice(TickerId tickerId, TickType field, double price, int canAutoExecute)
{
	PyLock lock;
	this->api->tickPrice(tickerId, field, price, canAutoExecute);
};

當IbWrapper的回調函數被觸發時,它會把返回的參數推送到Python環境中,並調用Python環境中的回調函數。並在回調函數中加入GIL全局鎖,防止Python崩潰。

ibGateway.py介紹

這裏簡要介紹一下vnpy中的ibGateway.py文件。該文件主要分為三部分內容:

  1. 一些VT類型和CTP類型的映射字典;
  2. IbGateway類,該類繼承VtGateway,主要將IB API做更上一層的封裝,提供接口給vnpy主程序;
  3. IbWrapper類,IB回調函數的具體實現;

下面以行情訂閱為例簡單介紹ibGateway.py的功能:

class IbGateway(VtGateway):
    """IB接口"""
...
    def subscribe(self, subscribeReq):
        """訂閱行情"""
        # 如果尚未連接行情,則將訂閱請求緩存下來後直接返回
        if not self.connected:
            self.subscribeReqDict[subscribeReq.symbol] = subscribeReq
            return

        contract = Contract()
        contract.localSymbol = str(subscribeReq.symbol)
        contract.exchange = exchangeMap.get(subscribeReq.exchange, '')
        contract.secType = productClassMap.get(subscribeReq.productClass, '')
        contract.currency = currencyMap.get(subscribeReq.currency, '')
        contract.expiry = subscribeReq.expiry
        contract.strike = subscribeReq.strikePrice
        contract.right = optionTypeMap.get(subscribeReq.optionType, '')

        # 獲取合約詳細信息
        self.tickerId += 1
        self.api.reqContractDetails(self.tickerId, contract)        

        # 創建合約對象並保存到字典中
        ct = VtContractData()
        ct.gatewayName = self.gatewayName
        ct.symbol = str(subscribeReq.symbol)
        ct.exchange = subscribeReq.exchange
        ct.vtSymbol = '.'.join([ct.symbol, ct.exchange])
        ct.productClass = subscribeReq.productClass
        self.contractDict[ct.vtSymbol] = ct

        # 訂閱行情
        self.tickerId += 1   
        self.api.reqMktData(self.tickerId, contract, '', False, TagValueList())

        # 創建Tick對象並保存到字典中
        tick = VtTickData()
        tick.symbol = subscribeReq.symbol
        tick.exchange = subscribeReq.exchange
        tick.vtSymbol = '.'.join([tick.symbol, tick.exchange])
        tick.gatewayName = self.gatewayName
        self.tickDict[self.tickerId] = tick   
        self.tickProductDict[self.tickerId] = subscribeReq.productClass
...

class IbWrapper(IbApi):
    """IB回調接口的實現"""
...
   def tickPrice(self, tickerId, field, price, canAutoExecute):
        """行情價格相關推送"""
        if field in tickFieldMap:
            # 對於股票、期貨等行情,有新價格推送時僅更新tick緩存
            # 只有當發生成交後,tickString更新最新成交價時才推送新的tick
            # 即bid/ask的價格變動並不會觸發新的tick推送
            tick = self.tickDict[tickerId]
            key = tickFieldMap[field]
            tick.__setattr__(key, price)

            # IB的外匯行情沒有成交價和時間,通過本地計算生成,同時立即推送
            if self.tickProductDict[tickerId] == PRODUCT_FOREX:
                tick.lastPrice = (tick.bidPrice1 + tick.askPrice1) / 2
                dt = datetime.now()
                tick.time = dt.strftime('%H:%M:%S.%f')
                tick.date = dt.strftime('%Y%m%d')

                # 行情數據更新
                newtick = copy(tick)
                self.gateway.onTick(newtick)
        else:
            print field

    def contractDetails(self, reqId, contractDetails):
        """合約查詢回報"""
        symbol = contractDetails.summary.localSymbol
        exchange = exchangeMapReverse.get(contractDetails.summary.exchange, EXCHANGE_UNKNOWN)
        vtSymbol = '.'.join([symbol, exchange])
        ct = self.contractDict.get(vtSymbol, None)

        if not ct:
            return

        ct.name = contractDetails.longName.decode('UTF-8')
        ct.priceTick = contractDetails.minTick        

        # 推送
        self.gateway.onContract(ct)
...

IbGateway類裏定義了subscribe函數,用於合約行情的訂閱。該函數需要傳入參數subscribeReq字典,裏面包含要訂閱行情合約的所有信息。在subscribe中會實例化一個合約對象,並將合約參數傳給該對象對應的字段。接著調用IB API 函數reqContractDetails函數,獲取合約的詳細信息,該主動函數會觸發回調函數contractDetails,在回調函數中保存收到的合約信息,並以EVENT_CONTRACT類型推送到事件引擎中實現合約的更新。然後調用IB API函數reqMktData,該函數會觸發回調函數tickPrice,在回調函數中會保存收到的價格信息,然後以EVENT_TICK推送給事件引擎。

參考文獻

  1. TWS API v9.72+: Trader Workstation API
  2. 如何學習盈透 api 的開發?,知乎
⚠️ **GitHub.com Fallback** ⚠️