跳转到内容

请求账户更新

reqAccountUpdates(True, account) 用来向 TWS 或 IB Gateway 发起账户更新订阅。发起成功后,TWS 会通过账户值、组合持仓、更新时间和下载结束回调把初始账户状态推给程序。

这个接口的重点不是“查询一次就结束”,而是“开启一个订阅”。程序收到初始批次后,订阅仍然存在,直到你主动调用 reqAccountUpdates(False, account) 取消。

Python API 的调用形式如下:

app.reqAccountUpdates(subscribe, acctCode)
参数中文含义常用取值说明
subscribe是否订阅True / FalseTrue 表示开始接收账户更新,False 表示停止接收账户更新。
acctCode账户代码例如 DU1234567要订阅的 IBKR 账户号。公开日志和文档里应脱敏。

在单账户环境里,部分示例会把 acctCode 传空字符串。但实际项目里更建议先通过 reqManagedAccts() 拿到这个会话可见账户,再把明确的账户号传给 reqAccountUpdates()。这样日志更清楚,多账户、顾问账户或之后排查时也更稳定。

调用前先确认这几件事:

检查项原因
TWS 或 IB Gateway 已登录API 不能绕过登录和重新认证。
API Socket 已启用TWS 里需要开启 ActiveX and Socket Clients。
程序已经收到 nextValidId()表示基础 socket 握手完成。
已知道要订阅的账户号可以从 managedAccounts() 回调获得。
没有另一个 reqAccountUpdates() 订阅需要保留传统账户更新订阅一次只适合一个账户。

如果只是想看少量字段,例如净清算值、购买力、可用资金,reqAccountSummary() 通常更轻量。如果要做账户状态面板或组合持仓面板,才更适合使用 reqAccountUpdates()

典型流程是:

连接 TWS / IB Gateway
-> 等待 nextValidId()
-> reqManagedAccts()
-> managedAccounts(accountsList)
-> reqAccountUpdates(True, account)
-> 等待 accountDownloadEnd(account)
-> reqAccountUpdates(False, account)
-> disconnect()

accountDownloadEnd(account) 只表示初始账户数据批次已经发完,不表示订阅自动结束。订阅是否结束取决于你是否发送了 reqAccountUpdates(False, account)

下面示例只演示“请求账户更新”的最小链路:先拿到账户号,再发起订阅,等初始批次结束后取消订阅。示例不会打印真实账户号、金额、持仓和盈亏。

import threading
import time
from ibapi.client import EClient
from 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=True
MANAGED_ACCOUNT_COUNT=1
ACCOUNT_ALIAS=ACCOUNT_1
SUBSCRIBE_SENT=True
DOWNLOAD_END_RECEIVED=True
DOWNLOAD_END_ACCOUNT_MATCHES=True
ACCOUNT_VALUE_CALLBACKS=183
PORTFOLIO_CALLBACKS=0
ACCOUNT_TIME_CALLBACKS=1
UNSUBSCRIBE_SENT=True
ROWS_BEFORE_UNSUBSCRIBE=183
ROWS_AFTER_UNSUBSCRIBE_WAIT=183
INFO_CODES=2100,2104,2106,2158
NON_INFO_ERROR_COUNT=0
IS_CONNECTED_AFTER_DISCONNECT=False

这次验证说明:

  • 账户列表可以正常返回,示例里脱敏显示为 ACCOUNT_1
  • reqAccountUpdates(True, account) 成功发起订阅,并收到了 accountDownloadEnd()
  • 账户环境收到了 183 次账户值回调、1 次更新时间回调。
  • 这个环境没有通过 updatePortfolio() 返回持仓行,这通常表示账户状态下没有可展示的组合持仓数据进入该回调。
  • reqAccountUpdates(False, account) 已发送,短时间等待后没有新增账户值或持仓回调。
  • 2100 是账户数据退订提示,210421062158 是常见数据服务连接状态提示;本次没有非信息类错误。

为什么要先调用 reqManagedAccts()

Section titled “为什么要先调用 reqManagedAccts()?”

因为 reqAccountUpdates() 需要一个账户代码。对新手来说,最稳妥的方式是让 TWS 先告诉程序这个会话可见哪些账户,再选其中一个账户去订阅。这样比手写账户号更不容易出错,也方便把真实账户号统一脱敏。

accountDownloadEnd() 之后还要取消订阅吗?

Section titled “accountDownloadEnd() 之后还要取消订阅吗?”

要。accountDownloadEnd() 表示初始批次已经下载完,不是取消订阅。程序退出前建议显式调用:

app.reqAccountUpdates(False, account)

2100 常见于账户数据订阅被取消或被新的账户数据请求替换。它更像状态提示,不一定是程序失败。判断是否失败时,要同时看是否收到了目标回调、是否有非信息类错误、程序是否仍然连接。

传统 reqAccountUpdates() 不适合同时维护多个账户订阅。多账户或模型组合场景应考虑 reqAccountUpdatesMulti() 相关接口。

IBKR Campus: TWS API Documentation