Skip to content
vn.py edited this page Sep 4, 2017 · 1 revision

作者:吴咸樾

目录

背景介绍

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 的开发?,知乎
You can’t perform that action at this time.