接收账户更新
发起 reqAccountUpdates(True, account) 后,账户更新不是通过一个返回值拿到的,而是通过多个 EWrapper 回调陆续推送。写程序时要把这几个回调当作同一条数据流处理:
updateAccountValue(...) 账户字段值updatePortfolio(...) 组合持仓行updateAccountTime(...) 账户更新时间accountDownloadEnd(...) 初始批次结束其中 accountDownloadEnd() 很关键:它表示这一轮初始账户值和组合持仓已经发送完毕。收到它之后,程序可以认为“首次加载完成”,但订阅本身仍然存在。
Python API 中常用的四个回调签名如下:
def updateAccountValue(self, key, val, currency, accountName): pass
def updatePortfolio( self, contract, position, marketPrice, marketValue, averageCost, unrealizedPNL, realizedPNL, accountName,): pass
def updateAccountTime(self, timeStamp): pass
def accountDownloadEnd(self, accountName): passupdateAccountValue()
Section titled “updateAccountValue()”updateAccountValue() 是账户更新里最常见的回调。它会返回现金、保证金、购买力、净清算值、Ledger 字段等账户级数据。
| 参数 | 中文含义 | 说明 |
|---|---|---|
key | 字段名 | 例如 NetLiquidation、BuyingPower、AvailableFunds、$LEDGER-CashBalance。 |
val | 字段值 | 字符串形式返回,可能是数字,也可能是文本。 |
currency | 币种 | 可能是 USD、BASE,也可能为空。 |
accountName | 账户号 | 真实账户代码,日志和网页展示时应脱敏。 |
val 不要一律当成浮点数。像 AccountType、AccountReady、$LEDGER-Currency 这类字段可能是文本;金额类字段才适合转成 Decimal。
updatePortfolio()
Section titled “updatePortfolio()”updatePortfolio() 返回组合持仓行。每个持仓合约通常对应一行,包含合约对象、持仓数量、市价、市值、成本和盈亏。
| 参数 | 中文含义 | 说明 |
|---|---|---|
contract | 合约对象 | 包含 symbol、secType、exchange、currency、conId 等信息。 |
position | 持仓数量 | 股票通常是股数,期权、期货等按对应合约单位理解。 |
marketPrice | 市场价格 | TWS 返回的估值价格。 |
marketValue | 市值 | 持仓估算市值。 |
averageCost | 平均成本 | 持仓平均成本。 |
unrealizedPNL | 未实现盈亏 | 未平仓盈亏。 |
realizedPNL | 已实现盈亏 | 已实现盈亏。 |
accountName | 账户号 | 真实账户代码,公开输出时应脱敏。 |
这个回调是否有数据,取决于账户是否有可返回的组合持仓。没有持仓行时,updatePortfolio() 可能一次都不会触发。
updateAccountTime()
Section titled “updateAccountTime()”updateAccountTime(timeStamp) 返回账户数据更新时间,参数是字符串。它适合用来在界面上显示“账户数据最后更新时间”,也可以作为调试时判断账户更新链路是否活跃的辅助信号。
不要把它当成交易所行情时间,也不要用它替代订单状态时间。账户更新时间只说明账户数据批次的更新时间。
accountDownloadEnd()
Section titled “accountDownloadEnd()”accountDownloadEnd(accountName) 表示初始账户数据批次已经发送完。常见用法是:
- 初始化账户页面时,等到这个回调后再把页面状态从“加载中”切换为“已加载”。
- 自动化脚本里,等到这个回调后再统计收到多少账户字段和持仓行。
- 需要退出程序时,先等它出现,再调用
reqAccountUpdates(False, account)取消订阅。
它不是取消订阅信号。取消订阅仍然要由程序主动发送。
Python 示例
Section titled “Python 示例”下面示例重点演示如何接收并分类四类账户更新回调。为了安全,示例只打印字段名、计数、币种和数据类型,不打印真实账户号、金额、持仓明细和盈亏。
import threadingimport timefrom collections import Counterfrom decimal import Decimal, InvalidOperation
from ibapi.client import EClientfrom ibapi.wrapper import EWrapper
INFO_CODES = {2100, 2104, 2106, 2158}
class ReceiveAccountUpdatesApp(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.events = [] 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): if len(self.events) < 12: self.events.append("updateAccountValue")
value_type = "empty" if val: try: Decimal(val) value_type = "decimal" except InvalidOperation: value_type = "text"
self.account_values.append((key, currency or "EMPTY", value_type))
def updatePortfolio( self, contract, position, marketPrice, marketValue, averageCost, unrealizedPNL, realizedPNL, accountName, ): if len(self.events) < 12: self.events.append("updatePortfolio")
self.portfolio_rows.append({ "symbol": contract.symbol, "secType": contract.secType, "currency": contract.currency, })
def updateAccountTime(self, timeStamp): if len(self.events) < 12: self.events.append("updateAccountTime") self.account_times.append(timeStamp)
def accountDownloadEnd(self, accountName): if len(self.events) < 12: self.events.append("accountDownloadEnd") 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 = ReceiveAccountUpdatesApp()
try: app.connect("127.0.0.1", 7497, clientId=971) thread = threading.Thread(target=app.run, daemon=True) thread.start()
connected = app.ready.wait(8) if not connected: 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 超时")
app.reqAccountUpdates(False, account) time.sleep(0.4)
finally: if app.isConnected(): app.disconnect()
key_counts = Counter(key for key, _, _ in app.account_values)currency_counts = Counter(currency for _, currency, _ in app.account_values)type_counts = Counter(value_type for _, _, value_type in app.account_values)
print("ACCOUNT_ALIAS=ACCOUNT_1")print(f"CONNECTED={connected}")print(f"DOWNLOAD_END_RECEIVED={app.download_end.is_set()}")print(f"ACCOUNT_VALUE_CALLBACKS={len(app.account_values)}")print(f"UNIQUE_ACCOUNT_VALUE_KEYS={len(key_counts)}")print("SAMPLE_KEYS=" + ",".join(sorted(key_counts)[:14]))print("CURRENCY_COUNTS=" + ",".join(f"{key}:{value}" for key, value in sorted(currency_counts.items())))print("VALUE_TYPE_COUNTS=" + ",".join(f"{key}:{value}" for key, value in sorted(type_counts.items())))print(f"PORTFOLIO_CALLBACKS={len(app.portfolio_rows)}")print(f"ACCOUNT_TIME_CALLBACKS={len(app.account_times)}")print("INFO_CODES=" + ",".join(map(str, sorted(set(app.info_codes)))))print(f"NON_INFO_ERROR_COUNT={len(app.errors_seen)}")使用 TWS 模拟账户检查时,脱敏后的输出如下:
CONNECTED=TrueACCOUNT_ALIAS=ACCOUNT_1DOWNLOAD_END_RECEIVED=TrueDOWNLOAD_END_ACCOUNT_MATCHES=TrueFIRST_EVENTS=updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValueACCOUNT_VALUE_CALLBACKS=183UNIQUE_ACCOUNT_VALUE_KEYS=158SAMPLE_KEYS=$LEDGER-AccountOrGroup,$LEDGER-AccruedCash,$LEDGER-CashBalance,$LEDGER-CorporateBondValue,$LEDGER-Cryptocurrency,$LEDGER-Currency,$LEDGER-ExchangeRate,$LEDGER-FundValue,$LEDGER-FutureOptionValue,$LEDGER-FuturesPNL,$LEDGER-FxCashBalance,$LEDGER-IssuerOptionValue,$LEDGER-MoneyMarketFundValue,$LEDGER-MutualFundValueCURRENCY_COUNTS=BASE:25,EMPTY:19,USD:139VALUE_TYPE_COUNTS=decimal:164,text:19PORTFOLIO_CALLBACKS=0PORTFOLIO_SECTYPES=NONEACCOUNT_TIME_CALLBACKS=1INFO_CODES=2100,2104,2106,2158NON_INFO_ERROR_COUNT=0IS_CONNECTED_AFTER_DISCONNECT=False这次返回里,账户值回调占主要部分。VALUE_TYPE_COUNTS 显示大多数值可以按数字理解,但仍有文本字段,所以解析时要按 key 决定类型,不能统一强转。
PORTFOLIO_CALLBACKS=0 只说明这次验证环境没有通过该订阅返回组合持仓行,不代表接口不支持持仓。真实账户、不同产品或不同持仓状态下,updatePortfolio() 可能会返回多行。
实际处理建议
Section titled “实际处理建议”| 场景 | 建议 |
|---|---|
| 展示账户字段 | 按 (accountName, key, currency) 做唯一键,后来的值覆盖旧值。 |
| 展示组合持仓 | 按 (accountName, contract.conId) 或更完整的合约标识做唯一键。 |
| 判断首次加载完成 | 等 accountDownloadEnd(accountName)。 |
| 处理字段类型 | 金额字段用 Decimal,文本字段保留字符串。 |
| 写日志 | 打印 key、币种、计数和脱敏账户别名,不打印真实金额和账户号。 |
| 退出程序 | 先 reqAccountUpdates(False, account),再断开连接。 |
收到 accountDownloadEnd() 后是不是不会再有更新?
Section titled “收到 accountDownloadEnd() 后是不是不会再有更新?”不是。它只表示初始批次结束。只要订阅还在,之后的账户状态变化仍可能继续推送。
updateAccountValue() 的 currency 为空是不是错误?
Section titled “updateAccountValue() 的 currency 为空是不是错误?”不一定。有些字段不是具体币种金额,例如账户类型、账户状态或某些汇总字段,currency 可能为空。解析时要把空币种作为正常情况处理。
val 可以直接转成 float 吗?
Section titled “val 可以直接转成 float 吗?”不建议。金额字段更适合用 Decimal;文本字段不能转数字;公开输出时也不应打印真实金额。
没有 updatePortfolio() 是否表示没有持仓?
Section titled “没有 updatePortfolio() 是否表示没有持仓?”只能说明这次账户更新流没有返回组合持仓行。要确认持仓状态,还可以使用持仓接口 reqPositions() 或组合相关接口交叉验证。