跳转到内容

接收账户更新

发起 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):
pass

updateAccountValue() 是账户更新里最常见的回调。它会返回现金、保证金、购买力、净清算值、Ledger 字段等账户级数据。

参数中文含义说明
key字段名例如 NetLiquidationBuyingPowerAvailableFunds$LEDGER-CashBalance
val字段值字符串形式返回,可能是数字,也可能是文本。
currency币种可能是 USDBASE,也可能为空。
accountName账户号真实账户代码,日志和网页展示时应脱敏。

val 不要一律当成浮点数。像 AccountTypeAccountReady$LEDGER-Currency 这类字段可能是文本;金额类字段才适合转成 Decimal

updatePortfolio() 返回组合持仓行。每个持仓合约通常对应一行,包含合约对象、持仓数量、市价、市值、成本和盈亏。

参数中文含义说明
contract合约对象包含 symbolsecTypeexchangecurrencyconId 等信息。
position持仓数量股票通常是股数,期权、期货等按对应合约单位理解。
marketPrice市场价格TWS 返回的估值价格。
marketValue市值持仓估算市值。
averageCost平均成本持仓平均成本。
unrealizedPNL未实现盈亏未平仓盈亏。
realizedPNL已实现盈亏已实现盈亏。
accountName账户号真实账户代码,公开输出时应脱敏。

这个回调是否有数据,取决于账户是否有可返回的组合持仓。没有持仓行时,updatePortfolio() 可能一次都不会触发。

updateAccountTime(timeStamp) 返回账户数据更新时间,参数是字符串。它适合用来在界面上显示“账户数据最后更新时间”,也可以作为调试时判断账户更新链路是否活跃的辅助信号。

不要把它当成交易所行情时间,也不要用它替代订单状态时间。账户更新时间只说明账户数据批次的更新时间。

accountDownloadEnd(accountName) 表示初始账户数据批次已经发送完。常见用法是:

  • 初始化账户页面时,等到这个回调后再把页面状态从“加载中”切换为“已加载”。
  • 自动化脚本里,等到这个回调后再统计收到多少账户字段和持仓行。
  • 需要退出程序时,先等它出现,再调用 reqAccountUpdates(False, account) 取消订阅。

它不是取消订阅信号。取消订阅仍然要由程序主动发送。

下面示例重点演示如何接收并分类四类账户更新回调。为了安全,示例只打印字段名、计数、币种和数据类型,不打印真实账户号、金额、持仓明细和盈亏。

import threading
import time
from collections import Counter
from decimal import Decimal, InvalidOperation
from ibapi.client import EClient
from 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=True
ACCOUNT_ALIAS=ACCOUNT_1
DOWNLOAD_END_RECEIVED=True
DOWNLOAD_END_ACCOUNT_MATCHES=True
FIRST_EVENTS=updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue > updateAccountValue
ACCOUNT_VALUE_CALLBACKS=183
UNIQUE_ACCOUNT_VALUE_KEYS=158
SAMPLE_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-MutualFundValue
CURRENCY_COUNTS=BASE:25,EMPTY:19,USD:139
VALUE_TYPE_COUNTS=decimal:164,text:19
PORTFOLIO_CALLBACKS=0
PORTFOLIO_SECTYPES=NONE
ACCOUNT_TIME_CALLBACKS=1
INFO_CODES=2100,2104,2106,2158
NON_INFO_ERROR_COUNT=0
IS_CONNECTED_AFTER_DISCONNECT=False

这次返回里,账户值回调占主要部分。VALUE_TYPE_COUNTS 显示大多数值可以按数字理解,但仍有文本字段,所以解析时要按 key 决定类型,不能统一强转。

PORTFOLIO_CALLBACKS=0 只说明这次验证环境没有通过该订阅返回组合持仓行,不代表接口不支持持仓。真实账户、不同产品或不同持仓状态下,updatePortfolio() 可能会返回多行。

场景建议
展示账户字段(accountName, key, currency) 做唯一键,后来的值覆盖旧值。
展示组合持仓(accountName, contract.conId) 或更完整的合约标识做唯一键。
判断首次加载完成accountDownloadEnd(accountName)
处理字段类型金额字段用 Decimal,文本字段保留字符串。
写日志打印 key、币种、计数和脱敏账户别名,不打印真实金额和账户号。
退出程序reqAccountUpdates(False, account),再断开连接。

收到 accountDownloadEnd() 后是不是不会再有更新?

Section titled “收到 accountDownloadEnd() 后是不是不会再有更新?”

不是。它只表示初始批次结束。只要订阅还在,之后的账户状态变化仍可能继续推送。

updateAccountValue()currency 为空是不是错误?

Section titled “updateAccountValue() 的 currency 为空是不是错误?”

不一定。有些字段不是具体币种金额,例如账户类型、账户状态或某些汇总字段,currency 可能为空。解析时要把空币种作为正常情况处理。

不建议。金额字段更适合用 Decimal;文本字段不能转数字;公开输出时也不应打印真实金额。

没有 updatePortfolio() 是否表示没有持仓?

Section titled “没有 updatePortfolio() 是否表示没有持仓?”

只能说明这次账户更新流没有返回组合持仓行。要确认持仓状态,还可以使用持仓接口 reqPositions() 或组合相关接口交叉验证。

IBKR Campus: TWS API Documentation