作者:吴咸樾
目录
IB API能成为许多量化平台的首选对接通道,不外乎一个原因:Created by traders, for traders。 公司创始人Thomas Peterffy的传奇背景就不多介绍了,关键在于IB背后的高频交易公司TimberHill,有这么一家市场上顶尖的玩家在为公司经纪相关的业务提供建议和各种技术支持,IB才可能同时给客户提供:
以上三点能做得比IB好的不能说没有,但是三点同时都能和IB竞争的似乎就不多见了:
所以综合来看,其实IB成功的原因和其他行业成功的公司也没多大区别,能够抓住客户的痛点并且长期提供高质来那个的服务才是关键。
使用IB API连接上的不是IB的交易服务器,而是运行于本机的TWS(带图形的交易平台)或者IB Gateway(只提供简单日志的交易路由,适合量化),即使对于FIX接口也是如此。如下图所示:
IB API提供了所有的底层源代码,用户在使用时需要自行编译底层API接口部分的组件,提供了一定灵活性的同时,也导致项目开发过程中容易遇到额外的编译问题。
IB API内建了一个比较大的内存缓冲区,回调函数即使阻塞一段时间也基本不会导致API崩溃,所以CTP类API开发时常用的一个回调函数推送的数据必须先进缓冲队列的设计模式就没有必要了,可以把一些可能耗时较长的逻辑直接写在回调函数中(出于程序代码架构的原因仍然不建议这么做,不过对于API封装来说省了很大的力气)。
IB API里回调函数(负责向用户的程序中推送数据)的工作线程需要用户自行创建和管理(国内几乎所有的API都是内部实现的),提供了极大的灵活性(比如用户可以使用类似协程的模式轻松实现一个线程同时管理8个API连接的推送),但是由于官方文档和Demo的缺失导致用户上手困难。
The Trader Workstation (TWS)是IB提供的交易平台软件,它支持全世界100多个市场的股票,期权,期货,外汇,债券,基金的交易,功能十分强大。而IB网关作为TWS的替代品更为小巧,但是从TWS API的角度看,两者并无差别:都是作为建立Socket连接的服务器。由于IB网关没有复杂的图形界面,能够提供比TWS更高的交易性能。
在用vnpy连接IB接口前,需要在TWS或IB网关上进行配置,选择菜单 Edit -> Global Configuration -> API -> Settings 确保 "Enable ActiveX and Socket Clients" 被勾选,如下图所示:
TWS API 包含以下几个重要组成成分:
下图以获取行情数据为例,简单介绍EClientSocket和EWrapper工作原理:
reqMktData()是请求获取市场行情数据的函数,EClientSocket类的方法,即主动函数。tickPrice()为Ewrapper类的方法,需要用户实现,用于接收市场行情数据。当调用reqMktData时,EClientSocket对象将会创造一个线程,从socket读取数据并触发Ewrapper类里的回调函数。
IB提供官方API开发的SDK,下载地址。直接下载稳定版。里面包含C++,Java和C#的代码和示例。下图为C++ API源文件:
其中:
以下是一个API的大体工作顺序:
//步骤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);
......
//步骤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);
};
......
//步骤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 f = boost::bind(&VnIbApi::run, this);
thread t(f);
this->worker = &t;
};
return r;
};
//步骤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会返回错误信息。
在进行行情订阅或下单之前,需要获取合约的细节信息。这需要调用主动函数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可以获取以下几种行情数据:
为了获得以上市场数据,需要在TWS中订阅相应交易品种的实时行情。需满足一下要求:
当用户请求某个品种的行情数据时,TWS将发起一个Market Data Line. Market Data Line代表活跃的用户行情数据请求。在默认情况下,一个用户最多能拥有100个Market Data Lines,即同时获取有100个品种的行情数据。
首先,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号。
市场深度数据通过调用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函数,每个请求需包含:
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;
}
可用IBApi.EClient.reqRealTimeBars函数请求实时K线数据,不过数据的粒度只能是5秒,即每5秒种发送一次OHLC值。数据接收和订阅撤销分别调用IBApi.EWrapper.realtimeBar和IBApi.EClient.cancelRealTimeBars函数。
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工作流程:
下面仍然以行情获取为例,说明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两个变量。
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 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崩溃。
这里简要介绍一下vnpy中的ibGateway.py文件。该文件主要分为三部分内容:
下面以行情订阅为例简单介绍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推送给事件引擎。