请求账户更新
reqAccountUpdates(True, account) 用来向 TWS 或 IB Gateway 发起账户更新订阅。发起成功后,TWS 会通过账户值、组合持仓、更新时间和下载结束回调把初始账户状态推给程序。
这个接口的重点不是“查询一次就结束”,而是“开启一个订阅”。程序收到初始批次后,订阅仍然存在,直到你主动调用 reqAccountUpdates(False, account) 取消。
Python API 的调用形式如下:
app.reqAccountUpdates(subscribe, acctCode)| 参数 | 中文含义 | 常用取值 | 说明 |
|---|---|---|---|
subscribe | 是否订阅 | True / False | True 表示开始接收账户更新,False 表示停止接收账户更新。 |
acctCode | 账户代码 | 例如 DU1234567 | 要订阅的 IBKR 账户号。公开日志和文档里应脱敏。 |
在单账户环境里,部分示例会把 acctCode 传空字符串。但实际项目里更建议先通过 reqManagedAccts() 拿到这个会话可见账户,再把明确的账户号传给 reqAccountUpdates()。这样日志更清楚,多账户、顾问账户或之后排查时也更稳定。
请求前要确认什么
Section titled “请求前要确认什么”调用前先确认这几件事:
| 检查项 | 原因 |
|---|---|
| TWS 或 IB Gateway 已登录 | API 不能绕过登录和重新认证。 |
| API Socket 已启用 | TWS 里需要开启 ActiveX and Socket Clients。 |
程序已经收到 nextValidId() | 表示基础 socket 握手完成。 |
| 已知道要订阅的账户号 | 可以从 managedAccounts() 回调获得。 |
没有另一个 reqAccountUpdates() 订阅需要保留 | 传统账户更新订阅一次只适合一个账户。 |
如果只是想看少量字段,例如净清算值、购买力、可用资金,reqAccountSummary() 通常更轻量。如果要做账户状态面板或组合持仓面板,才更适合使用 reqAccountUpdates()。
最小请求流程
Section titled “最小请求流程”典型流程是:
连接 TWS / IB Gateway -> 等待 nextValidId() -> reqManagedAccts() -> managedAccounts(accountsList) -> reqAccountUpdates(True, account) -> 等待 accountDownloadEnd(account) -> reqAccountUpdates(False, account) -> disconnect()accountDownloadEnd(account) 只表示初始账户数据批次已经发完,不表示订阅自动结束。订阅是否结束取决于你是否发送了 reqAccountUpdates(False, account)。
Python 示例
Section titled “Python 示例”下面示例只演示“请求账户更新”的最小链路:先拿到账户号,再发起订阅,等初始批次结束后取消订阅。示例不会打印真实账户号、金额、持仓和盈亏。
import threadingimport timefrom ibapi.client import EClientfrom ibapi.wrapper import EWrapper
INFO_CODES = {2100, 2104, 2106, 2158}
class RequestAccountUpdatesApp(EWrapper, EClient): def __init__(self): EClient.__init__(self, self) self.ready = threading.Event() self.managed_ready = threading.Event() self.download_end = threading.Event() self.managed_accounts = [] self.account_value_count = 0 self.portfolio_count = 0 self.account_time_count = 0 self.download_end_account = None self.info_codes = [] self.errors_seen = []
def nextValidId(self, orderId): # 收到这个回调,说明基础 API 连接已经建立。 self.ready.set()
def managedAccounts(self, accountsList): # accountsList 是逗号分隔的账户号列表。公开输出时不要直接打印原始账户号。 self.managed_accounts = [item for item in accountsList.split(",") if item] self.managed_ready.set()
def updateAccountValue(self, key, val, currency, accountName): # 这里只计数,不打印 val 和 accountName,避免泄露资金和账户信息。 self.account_value_count += 1
def updatePortfolio( self, contract, position, marketPrice, marketValue, averageCost, unrealizedPNL, realizedPNL, accountName, ): # 持仓、成本和盈亏都属于敏感数据,示例只统计回调数量。 self.portfolio_count += 1
def updateAccountTime(self, timeStamp): self.account_time_count += 1
def accountDownloadEnd(self, accountName): self.download_end_account = accountName self.download_end.set()
def error(self, reqId, errorTime, errorCode, errorString, advancedOrderRejectJson=""): if errorCode in INFO_CODES: self.info_codes.append(errorCode) else: self.errors_seen.append((reqId, errorCode, errorString))
app = RequestAccountUpdatesApp()
try: app.connect("127.0.0.1", 7497, clientId=970)
thread = threading.Thread(target=app.run, daemon=True) thread.start()
if not app.ready.wait(8): raise RuntimeError("等待 nextValidId 超时,请检查 TWS API 设置和端口")
app.reqManagedAccts()
if not app.managed_ready.wait(8): raise RuntimeError("等待 managedAccounts 超时")
if not app.managed_accounts: raise RuntimeError("这个 TWS 会话没有返回可见账户")
account = app.managed_accounts[0]
# 开始订阅账户更新。 app.reqAccountUpdates(True, account)
if not app.download_end.wait(8): raise RuntimeError("等待 accountDownloadEnd 超时")
rows_before_unsubscribe = app.account_value_count + app.portfolio_count
# 停止订阅账户更新。 app.reqAccountUpdates(False, account) time.sleep(0.4)
rows_after_unsubscribe = app.account_value_count + app.portfolio_count
finally: if app.isConnected(): app.disconnect()
print("CONNECTED=True")print(f"ACCOUNT_ALIAS={'ACCOUNT_1' if app.managed_accounts else 'NONE'}")print(f"MANAGED_ACCOUNT_COUNT={len(app.managed_accounts)}")print(f"SUBSCRIBE_SENT={bool(app.managed_accounts)}")print(f"DOWNLOAD_END_RECEIVED={app.download_end.is_set()}")print(f"DOWNLOAD_END_ACCOUNT_MATCHES={app.download_end_account == app.managed_accounts[0] if app.managed_accounts else False}")print(f"ACCOUNT_VALUE_CALLBACKS={app.account_value_count}")print(f"PORTFOLIO_CALLBACKS={app.portfolio_count}")print(f"ACCOUNT_TIME_CALLBACKS={app.account_time_count}")print(f"UNSUBSCRIBE_SENT={bool(app.managed_accounts)}")print(f"ROWS_BEFORE_UNSUBSCRIBE={rows_before_unsubscribe}")print(f"ROWS_AFTER_UNSUBSCRIBE_WAIT={rows_after_unsubscribe}")print("INFO_CODES=" + (",".join(map(str, sorted(set(app.info_codes)))) if app.info_codes else "NONE"))print(f"NON_INFO_ERROR_COUNT={len(app.errors_seen)}")print(f"IS_CONNECTED_AFTER_DISCONNECT={app.isConnected()}")使用 TWS 模拟账户、127.0.0.1:7497 和独立 clientId 检查时,脱敏后的输出如下:
CONNECTED=TrueMANAGED_ACCOUNT_COUNT=1ACCOUNT_ALIAS=ACCOUNT_1SUBSCRIBE_SENT=TrueDOWNLOAD_END_RECEIVED=TrueDOWNLOAD_END_ACCOUNT_MATCHES=TrueACCOUNT_VALUE_CALLBACKS=183PORTFOLIO_CALLBACKS=0ACCOUNT_TIME_CALLBACKS=1UNSUBSCRIBE_SENT=TrueROWS_BEFORE_UNSUBSCRIBE=183ROWS_AFTER_UNSUBSCRIBE_WAIT=183INFO_CODES=2100,2104,2106,2158NON_INFO_ERROR_COUNT=0IS_CONNECTED_AFTER_DISCONNECT=False这次验证说明:
- 账户列表可以正常返回,示例里脱敏显示为
ACCOUNT_1。 reqAccountUpdates(True, account)成功发起订阅,并收到了accountDownloadEnd()。- 账户环境收到了 183 次账户值回调、1 次更新时间回调。
- 这个环境没有通过
updatePortfolio()返回持仓行,这通常表示账户状态下没有可展示的组合持仓数据进入该回调。 reqAccountUpdates(False, account)已发送,短时间等待后没有新增账户值或持仓回调。2100是账户数据退订提示,2104、2106、2158是常见数据服务连接状态提示;本次没有非信息类错误。
为什么要先调用 reqManagedAccts()?
Section titled “为什么要先调用 reqManagedAccts()?”因为 reqAccountUpdates() 需要一个账户代码。对新手来说,最稳妥的方式是让 TWS 先告诉程序这个会话可见哪些账户,再选其中一个账户去订阅。这样比手写账户号更不容易出错,也方便把真实账户号统一脱敏。
accountDownloadEnd() 之后还要取消订阅吗?
Section titled “accountDownloadEnd() 之后还要取消订阅吗?”要。accountDownloadEnd() 表示初始批次已经下载完,不是取消订阅。程序退出前建议显式调用:
app.reqAccountUpdates(False, account)为什么会看到 2100?
Section titled “为什么会看到 2100?”2100 常见于账户数据订阅被取消或被新的账户数据请求替换。它更像状态提示,不一定是程序失败。判断是否失败时,要同时看是否收到了目标回调、是否有非信息类错误、程序是否仍然连接。
可以同时订阅多个账户吗?
Section titled “可以同时订阅多个账户吗?”传统 reqAccountUpdates() 不适合同时维护多个账户订阅。多账户或模型组合场景应考虑 reqAccountUpdatesMulti() 相关接口。