接收账户摘要
发送 reqAccountSummary() 之后,账户摘要不会作为函数返回值出现,而是通过回调陆续送回来。接收账户摘要主要处理两个回调:
accountSummary(reqId, account, tag, value, currency)accountSummaryEnd(reqId)accountSummary() 每次只返回一个账户的一个字段。accountSummaryEnd() 表示本次请求的初始账户摘要批次已经发送完毕。
Python API 中需要在自己的 EWrapper 子类里覆盖这两个方法:
def accountSummary(self, reqId, account, tag, value, currency): ...
def accountSummaryEnd(self, reqId): ...本地官方源码中的定义是:
def accountSummary(self, reqId: int, account: str, tag: str, value: str, currency: str): ...
def accountSummaryEnd(self, reqId: int): ...注意 value 是字符串。即使它看起来像金额或数字,也应在需要计算时再显式转换。
accountSummary 字段
Section titled “accountSummary 字段”| 字段 | 中文含义 | 说明 |
|---|---|---|
reqId | 请求 ID | 对应发送 reqAccountSummary() 时传入的 reqId |
account | 账户号 | 敏感信息,公开日志里应脱敏 |
tag | 字段标签 | 例如 NetLiquidation、BuyingPower |
value | 字段值 | 字符串类型,金额计算时建议转成 Decimal |
currency | 币种 | 可能为空,例如 AccountType 或 Cushion |
如果请求 5 个标签、这个会话可见 1 个账户,最少可能收到 5 次 accountSummary()。如果是多个账户,或 TWS 推送了更新,回调次数会更多。
accountSummaryEnd 含义
Section titled “accountSummaryEnd 含义”accountSummaryEnd(reqId) 只表示“这一轮初始数据已经发完”。它不代表订阅已经自动取消,也不代表之后一定不会再有更新。
常见处理方式:
| 场景 | 处理方式 |
|---|---|
| 只取一次账户摘要 | 收到 accountSummaryEnd() 后整理结果,再调用 cancelAccountSummary(reqId) |
| 持续显示账户摘要 | 收到 accountSummaryEnd() 后继续保持连接,之后的 accountSummary() 更新仍按同样方式处理 |
| 多个请求同时存在 | 用 reqId 分开保存不同请求的数据 |
官方账户摘要订阅的更新频率约为 3 分钟,不能由 API 客户端自行调快。需要更细的账户、持仓或组合变化时,应结合账户更新、持仓和组合相关接口。
推荐整理结构
Section titled “推荐整理结构”最实用的结构是按账户、字段和币种保存最新值:
latest[(account, tag, currency)] = value或者整理成嵌套字典:
summary[account][tag] = { "value": value, "currency": currency,}不要简单地把每一次回调都追加成一行长期保存。账户摘要是订阅式数据,同一个字段可能先返回初始值,再返回更新值。做界面展示和策略判断时,通常应使用最新值。
Python 接收示例
Section titled “Python 接收示例”下面示例展示如何接收回调、判断 value 是否像数字、等待 accountSummaryEnd(),并把结果按字段去重。示例中的公开输出不会打印真实账户号和真实金额。
import threadingfrom collections import defaultdictfrom decimal import Decimal, InvalidOperationfrom ibapi.client import EClientfrom ibapi.wrapper import EWrapper
REQ_ID = 9004TAGS = "AccountType,NetLiquidation,BuyingPower,AvailableFunds,Cushion"
class ReceivingAccountSummaryApp(EWrapper, EClient): def __init__(self): EClient.__init__(self, self) self.ready = threading.Event() self.done = threading.Event() self.rows = [] self.end_req_id = None self.errors_seen = []
def nextValidId(self, orderId): self.ready.set()
def accountSummary(self, reqId, account, tag, value, currency): value_type = "decimal" if self._is_decimal(value) else "text"
self.rows.append({ "reqId": reqId, "account": account, "tag": tag, "valueType": value_type, "currency": currency or "EMPTY", })
def accountSummaryEnd(self, reqId): self.end_req_id = reqId self.done.set()
def error(self, reqId, errorTime, errorCode, errorString, advancedOrderRejectJson=""): if errorCode not in (2104, 2106, 2158): self.errors_seen.append((reqId, errorCode, errorString))
@staticmethod def _is_decimal(value): try: Decimal(value) return True except (InvalidOperation, ValueError): return False
app = ReceivingAccountSummaryApp()
try: app.connect("127.0.0.1", 7497, clientId=963)
thread = threading.Thread(target=app.run, daemon=True) thread.start()
if not app.ready.wait(8): raise RuntimeError("等待 nextValidId 超时")
app.reqAccountSummary(REQ_ID, "All", TAGS)
if not app.done.wait(8): raise RuntimeError("等待 accountSummaryEnd 超时")
app.cancelAccountSummary(REQ_ID)
latest = {} duplicates = defaultdict(int)
for row in app.rows: key = (row["account"], row["tag"], row["currency"]) duplicates[key] += 1 latest[key] = row
print(f"RAW_CALLBACK_ROWS={len(app.rows)}") print(f"UNIQUE_FIELD_ROWS={len(latest)}")
finally: if app.isConnected(): app.disconnect()使用TWS 模拟账户、127.0.0.1:7497 和独立 clientId 检查时,脱敏后的输出如下:
CONNECTED=TrueREQUEST_ID=9004END_REQUEST_ID=9004END_MATCHES_REQUEST=TrueREQUESTED_TAGS=AccountType,NetLiquidation,BuyingPower,AvailableFunds,CushionRAW_CALLBACK_ROWS=10UNIQUE_FIELD_ROWS=5TAGS_RECEIVED=AccountType,AvailableFunds,BuyingPower,Cushion,NetLiquidationCURRENCIES=EMPTY,USDVALUE_TYPES=decimal,textACCOUNT_ALIAS=ACCOUNT_1;FIELD=AccountType;CURRENCY=EMPTY;VALUE_TYPE=text;VALUE=<redacted>;CALLBACK_COUNT=2ACCOUNT_ALIAS=ACCOUNT_1;FIELD=AvailableFunds;CURRENCY=USD;VALUE_TYPE=decimal;VALUE=<redacted>;CALLBACK_COUNT=2ACCOUNT_ALIAS=ACCOUNT_1;FIELD=BuyingPower;CURRENCY=USD;VALUE_TYPE=decimal;VALUE=<redacted>;CALLBACK_COUNT=2ACCOUNT_ALIAS=ACCOUNT_1;FIELD=Cushion;CURRENCY=EMPTY;VALUE_TYPE=decimal;VALUE=<redacted>;CALLBACK_COUNT=2ACCOUNT_ALIAS=ACCOUNT_1;FIELD=NetLiquidation;CURRENCY=USD;VALUE_TYPE=decimal;VALUE=<redacted>;CALLBACK_COUNT=2NON_INFO_ERROR_COUNT=0IS_CONNECTED_AFTER_DISCONNECT=False这次请求了 5 个字段,实际收到了 10 次原始回调,去重后是 5 个字段。账户摘要是订阅型数据,同一个字段可能先返回初始值,再返回更新值。程序应按 (account, tag, currency) 保存最新值,不应把重复回调当成异常。
value 转换建议
Section titled “value 转换建议”value 在官方回调里是字符串。处理方式可以按字段类型区分:
| 字段类型 | 示例 | 建议 |
|---|---|---|
| 文本字段 | AccountType | 保留字符串 |
| 金额字段 | NetLiquidation、AvailableFunds | 用 Decimal(value) 转换 |
| 比例字段 | Cushion、Leverage | 用 Decimal(value) 转换,并明确显示口径 |
| 时间字段 | LookAheadNextChange | 按官方返回格式解析,不要当金额处理 |
财务金额不建议直接使用浮点数长期计算。浮点数适合临时展示,不适合做严肃对账或风控判断。
账户摘要里包含账户号、现金、净值、保证金和购买力,公开环境必须谨慎处理:
| 数据 | 公开展示建议 |
|---|---|
account | 替换成 ACCOUNT_1、ACCOUNT_2 |
value | 金额和权益值替换成 <redacted> |
currency | 可保留,例如 USD |
tag | 可保留,字段名本身不敏感 |
| 错误码 | 可保留,但不要附带账户资产明细 |
如果用户需要排查问题,优先提供字段名、请求 ID、错误码、是否收到 accountSummaryEnd(),不要直接贴完整账户摘要。
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 回调行数比字段数多 | 初始快照和更新都触发了 accountSummary() | 按账户、标签和币种保存最新值 |
没收到 accountSummaryEnd() | TWS 未允许 API、请求参数错误、连接断开 | 检查端口、API 设置、groupName 和 tags |
END_REQUEST_ID 和请求 ID 不一致 | 多个请求混在一起处理,或变量使用错误 | 按 reqId 分桶保存每个请求的数据 |
value 转数字失败 | 字段是文本,或返回值不是标准数字格式 | 按字段类型转换,不要统一强转 |
| 币种为空 | 字段本身不是金额字段 | 允许空币种,展示为 EMPTY 或空字符串 |