实时行情限制
实时行情不是只要能连接 TWS 就一定能拿到。API 连接、合约定义、行情权限和订阅容量是四件不同的事:nextValidId() 到达只能说明 Socket 已经连上,reqContractDetails() 能返回只能说明合约能识别,真正能不能收到实时价格,还要看账户是否有对应交易所、品种、接口类型的数据权限,以及是否还有可用的行情行数。
官方参考:
| 限制 | 中文解释 | 常见表现 |
|---|---|---|
| 行情权限 | 账户需要对应交易所、品种和 API 场景的数据权限。美股常见会涉及 NASDAQ、NYSE、ARCA、ISLAND 等交易所权限。 | 420、10089、10189,或者请求发出后没有价格回调。 |
| 行情行数 | reqMktData()、reqMktDepth()、reqRealTimeBars() 等持续订阅都会占用行情行数。TWS 界面里打开的报价和 API 订阅会一起占用额度。 | 新请求被拒绝、没有新推送、提示超出市场数据订阅数量。 |
| 实时 / 延迟类型 | reqMarketDataType(3) 可以让后续 reqMktData() 优先使用延迟行情;如果账户已有实时权限,TWS 可能仍返回实时数据。 | 收到 marketDataType() 回调,或收到延迟 tick 类型。 |
| 接口差异 | reqMktData()、reqTickByTickData()、reqRealTimeBars() 都属于实时相关接口,但权限、限频和回调形式不同。 | 一档报价可用,不代表 5 秒线、二档盘口或逐笔数据一定可用。 |
| 小周期限频 | 实时 5 秒线属于持续订阅,同时受行情行数和小周期请求限频影响。官方要求 10 分钟内不要发起超过 60 个新的实时 5 秒线请求。 | 批量重连、频繁换合约时更容易触发限制。 |
| 合约定义 | 合约不唯一、交易所字段不合适或币种错误时,行情请求前就可能失败。 | 200、没有唯一合约,或请求落到不期望的数据源。 |
| 订阅生命周期 | 实时订阅会一直占用资源,程序结束前应主动取消。 | 达到订阅上限,或取消未建立成功的订阅时出现 300。 |
| 监管快照 | 美国市场监管快照通过 reqMktData() 的 regulatorySnapshot=True 请求,官方说明这类请求可能产生费用。 | 不适合随意轮询;需要在界面上明确告知用户。 |
参考边界样例
Section titled “参考边界样例”使用 TWS API 请求 AAPL 的实时 5 秒线,连接和请求都能发出,但账户没有对应数据源的 API 行情权限:
CONNECTED=TrueREQUEST_SENT=TrueCANCEL_SENT=TrueBAR_ROWS=0ERROR=reqId=97201;code=420;msg=实时查询无效:No market data permissions for ISLAND STK. 请求的市场数据对于API来说需要额外订阅。点击“市场数据连接”对话框中的链接获取更多详情。ERROR=reqId=97201;code=300;msg=无法使用tickerId找到EId::97201这里的重点是:Socket 连接没有问题,请求也发出去了;真正阻塞的是行情权限。后面的 300 不是根因,它通常发生在取消订阅时:前面的请求已经因为权限错误没有建立成功,取消时 TWS 就找不到对应的订阅编号。
如果看到 10089 或 10189,含义也类似:代码已经到达 TWS,但账户没有该数据源或该接口场景所需的权限。不同 TWS 版本、账户类型、交易所和请求接口,错误码可能表现为 420、10089 或 10189,排查时要把错误码、合约、交易所和请求类型一起记录。
新手排查顺序
Section titled “新手排查顺序”| 步骤 | 检查什么 |
|---|---|
| 1 | nextValidId() 是否到达,确认 API 连接完成 |
| 2 | 合约是否能用 reqContractDetails() 查到唯一结果 |
| 3 | 是否调用了正确的实时接口:顶层报价用 reqMktData(),5 秒线用 reqRealTimeBars(),逐笔用 reqTickByTickData() |
| 4 | 请求是否返回 420、10089 等权限错误 |
| 5 | 是否需要先尝试 reqMarketDataType(3) 获取延迟报价 |
| 6 | 是否已经发送取消请求释放订阅 |
| 7 | 是否超出账户可用行情行数 |
不要把权限错误写成“代码失败”。真实项目里应把错误码、合约、交易所、请求类型和市场数据类型记录下来,让用户知道应该开通数据权限、切换延迟行情,还是换一个可用合约测试。
| 错误码 | 常见含义 | 处理方式 |
|---|---|---|
10089 | 这类市场数据对 API 请求需要额外订阅。 | 到 IBKR 的市场数据订阅页面确认交易所、品种和 API 数据权限。 |
10189 | 逐笔数据请求失败,常见原因是缺少对应实时逐笔行情权限,或产品不支持该逐笔类型。 | 检查交易所权限、产品类型和 tickType,不要把空回调当成真实无成交。 |
420 | 没有对应市场数据权限,或请求的数据源不可用。 | 换合约、换交易所字段、尝试延迟行情,或补齐权限。 |
200 | 合约定义不唯一或找不到。 | 先用合约详情接口确认 conId、primaryExchange、currency。 |
300 | 取消订阅时找不到对应请求编号。 | 如果前面已经权限失败,这个错误可以作为后续结果记录,不要当成根因。 |
101 / 订阅数量提示 | 行情行数达到限制。 | 取消不再使用的订阅,减少 TWS 界面打开的报价,或提升账户行情额度。 |
行情请求代码里至少要记录这些字段:
| 字段 | 为什么要记录 |
|---|---|
reqId | 对齐请求、回调和错误。 |
symbol / secType / exchange / currency | 判断是不是合约写错或落到了不期望的交易所。 |
marketDataType | 区分实时、冻结、延迟、延迟冻结。 |
whatToShow | 5 秒线和历史数据会用到,区分 TRADES、MIDPOINT、BID、ASK 等。 |
snapshot / regulatorySnapshot | 区分快照、流式订阅和监管快照,避免把可能收费的请求当成普通订阅。 |
errorCode / errorMessage | 让用户知道是权限、合约、连接还是取消订阅问题。 |
最小错误处理思路
Section titled “最小错误处理思路”实时行情代码不要只写一个 print(price)。更稳的写法是把订阅状态做成一张表:
active_market_data = {}
def mark_requested(req_id, contract, request_type): active_market_data[req_id] = { "symbol": contract.symbol, "secType": contract.secType, "exchange": contract.exchange, "currency": contract.currency, "requestType": request_type, "status": "requested", }
def mark_error(req_id, code, message): row = active_market_data.get(req_id) if row is None: return row["status"] = "error" row["errorCode"] = code row["errorMessage"] = message
def mark_cancelled(req_id): active_market_data.pop(req_id, None)这样用户界面里可以明确显示:某个请求是已发送、已收到行情、权限不足、合约错误,还是已经取消。对开发工具来说,这比只在终端里看一行错误更容易定位问题。