账户更新总览
reqAccountUpdates() 用来订阅某个账户的账户值、组合持仓和更新时间。它比账户摘要更偏向“账户页面数据流”,会通过多个回调持续推送数据:
reqAccountUpdates(True, account) -> updateAccountValue(key, value, currency, accountName) -> updatePortfolio(contract, position, marketPrice, marketValue, averageCost, unrealizedPNL, realizedPNL, accountName) -> updateAccountTime(timeStamp) -> accountDownloadEnd(accountName)
reqAccountUpdates(False, account)如果你要做账户面板、组合持仓面板、资金字段观察或下单前账户状态检查,这组接口很重要。
和账户摘要的区别
Section titled “和账户摘要的区别”账户摘要和账户更新都能返回账户相关数据,但定位不同:
| 接口 | 主要用途 | 典型回调 |
|---|---|---|
reqAccountSummary() | 请求一组摘要标签,例如净清算值、购买力、可用资金 | accountSummary()、accountSummaryEnd() |
reqAccountUpdates() | 订阅某个账户的账户值、组合持仓和更新时间 | updateAccountValue()、updatePortfolio()、updateAccountTime()、accountDownloadEnd() |
账户摘要适合做轻量概览。账户更新更适合做账户状态页,因为它除了账户值,还会推送组合持仓行。
Python API 中的调用形式如下:
app.reqAccountUpdates(subscribe, acctCode)| 参数 | 中文含义 | 说明 |
|---|---|---|
subscribe | 是否订阅 | True 开始接收账户更新,False 停止接收 |
acctCode | 账户代码 | 登录会话可见的账户号;公开日志中应脱敏 |
普通单账户环境下,可以先通过 reqManagedAccts() 获取可见账户,再传给 reqAccountUpdates(True, account)。
| 回调 | 中文含义 | 说明 |
|---|---|---|
updateAccountValue() | 账户字段值 | 例如现金、保证金、购买力、Ledger 字段等 |
updatePortfolio() | 组合持仓行 | 每个持仓合约一行,包含持仓数量、市价、市值、成本和盈亏 |
updateAccountTime() | 账户更新时间 | 返回账户更新批次的时间字符串 |
accountDownloadEnd() | 初始批次结束 | 表示这一轮账户值和组合持仓初始数据已经发完 |
accountDownloadEnd() 和 accountSummaryEnd() 类似,只代表初始批次结束,不代表订阅已经自动停止。
单账户订阅限制
Section titled “单账户订阅限制”TWS API 的传统 reqAccountUpdates() 一次只能订阅一个账户。官方错误码里常见的 2100 就和这个限制有关:当新的账户数据请求替换旧请求,或取消订阅时,TWS 可能提示账户数据已退订。
这不是普通网络断线错误。处理时要关注两点:
- 程序里清楚记录正在订阅哪个账户。
- 如果要换账户,先取消旧订阅,再发起新订阅。
多账户或模型组合场景,应使用 reqAccountUpdatesMulti(),这样每条回调都会带有 reqId、账户和模型代码,程序更容易归类。
Python 总览示例
Section titled “Python 总览示例”下面示例会:
- 连接TWS 模拟账户。
- 通过
reqManagedAccts()获取可见账户。 - 订阅
reqAccountUpdates(True, account)。 - 等待
accountDownloadEnd()。 - 取消订阅并断开连接。
示例输出只保留字段名、数量和脱敏后的账户别名。
import threadingimport timefrom collections import Counterfrom ibapi.client import EClientfrom ibapi.wrapper import EWrapper
INFO_CODES = {2100, 2104, 2106, 2158}
class AccountUpdatesOverviewApp(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_values = [] self.portfolio_rows = [] self.account_times = [] self.download_end_account = None self.info_codes = [] self.errors_seen = []
def nextValidId(self, orderId): self.ready.set()
def managedAccounts(self, 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 都可能包含敏感信息,公开输出里只保留 key 和币种。 self.account_values.append((key, currency or "EMPTY", accountName))
def updatePortfolio( self, contract, position, marketPrice, marketValue, averageCost, unrealizedPNL, realizedPNL, accountName, ): # 持仓、盈亏、成本都属于敏感数据。公开输出只统计数量和证券类型。 self.portfolio_rows.append({ "secType": contract.secType, "currency": contract.currency, "accountName": accountName, })
def updateAccountTime(self, timeStamp): self.account_times.append(timeStamp)
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 = AccountUpdatesOverviewApp()
try: app.connect("127.0.0.1", 7497, clientId=969)
thread = threading.Thread(target=app.run, daemon=True) thread.start()
if not app.ready.wait(8): raise RuntimeError("等待 nextValidId 超时")
app.reqManagedAccts()
if not app.managed_ready.wait(8): raise RuntimeError("等待 managedAccounts 超时")
account = app.managed_accounts[0] app.reqAccountUpdates(True, account)
if not app.download_end.wait(8): raise RuntimeError("等待 accountDownloadEnd 超时")
rows_before_cancel = len(app.account_values) + len(app.portfolio_rows)
app.reqAccountUpdates(False, account) time.sleep(0.3)
rows_after_cancel = len(app.account_values) + len(app.portfolio_rows)
finally: if app.isConnected(): app.disconnect()使用TWS 模拟账户、127.0.0.1:7497 和独立 clientId 检查时,脱敏后的输出如下:
CONNECTED=TrueACCOUNT_ALIAS=ACCOUNT_1MANAGED_ACCOUNT_COUNT=1SUBSCRIBE_SENT=TrueACCOUNT_VALUE_ROW_COUNT=183ACCOUNT_VALUE_UNIQUE_KEYS=158PORTFOLIO_ROW_COUNT=0ACCOUNT_TIME_COUNT=1DOWNLOAD_END_RECEIVED=TrueDOWNLOAD_END_ACCOUNT_MATCHES=TrueCURRENCIES=BASE,EMPTY,USDPORTFOLIO_SECURITY_TYPES=NONESAMPLE_ACCOUNT_VALUE_KEYS=$LEDGER-AccountOrGroup,$LEDGER-AccruedCash,$LEDGER-CashBalance,$LEDGER-CorporateBondValue,$LEDGER-Cryptocurrency,$LEDGER-Currency,$LEDGER-ExchangeRate,$LEDGER-FundValue,$LEDGER-FutureOptionValue,$LEDGER-FuturesPNL,$LEDGER-FxCashBalance,$LEDGER-IssuerOptionValueKEY_PREFIX_COUNTS=$LEDGER:50,AccountCode:1,AccountReady:1,AccountType:1,AccruedCash:4,AccruedDividend:4,AvailableFunds:4,Billable:4,BuyingPower:1,ColumnPrio:3,Cushion:1,EquityWithLoanValue:4UNSUBSCRIBE_SENT=TrueROWS_BEFORE_UNSUBSCRIBE=183ROWS_AFTER_UNSUBSCRIBE_WAIT=183INFO_CODES=2100,2104,2106,2158NON_INFO_ERROR_COUNT=0IS_CONNECTED_AFTER_DISCONNECT=FalseTWS 模拟账户返回了 183 行账户值、158 个去重字段、1 次更新时间,并收到 accountDownloadEnd()。组合持仓行是 0,表示这个环境没有通过该回调返回持仓行;这不代表接口不能返回持仓,只是账户状态下没有持仓数据进入 updatePortfolio()。
INFO_CODES 中的 2100 是账户数据退订提示,2104、2106、2158 是常见数据服务连接状态提示。本例没有非信息类错误。
适合用来做什么
Section titled “适合用来做什么”| 需求 | 是否适合 |
|---|---|
| 显示账户资金字段 | 适合 |
| 显示组合持仓行 | 适合,但取决于账户是否有持仓数据 |
| 下单前检查购买力和可用资金 | 适合,但还应结合订单预检查 |
| 只查少量摘要字段 | 可以,但 reqAccountSummary() 更轻量 |
| 多账户同时订阅 | 不适合传统 reqAccountUpdates(),应看 multi 接口 |
| 成交流水查询 | 不适合,应使用成交接口 |
账户更新比账户摘要更容易泄露敏感信息,因为它可能包含账户号、现金、保证金、持仓、市值、成本和盈亏。
公开日志建议只保留:
- 回调数量。
- 字段名。
- 币种。
- 是否收到
accountDownloadEnd()。 - 脱敏账户别名,例如
ACCOUNT_1。
不要公开打印真实账户号、真实金额、持仓合约、成本和盈亏。
本组页面导览
Section titled “本组页面导览”账户更新这一组包含以下页面:
| 页面 | 内容 |
|---|---|
| 请求账户更新 | reqAccountUpdates(True, account) 的参数、账户号来源和订阅限制 |
| 接收账户更新 | updateAccountValue()、updatePortfolio()、updateAccountTime()、accountDownloadEnd() 的字段解释 |
| 账户值字段 key | 常见 account value key 的中文含义 |
| 取消账户更新 | reqAccountUpdates(False, account) 的取消流程和常见提示 |