跳转到内容

接收账户摘要

发送 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 是字符串。即使它看起来像金额或数字,也应在需要计算时再显式转换。

字段中文含义说明
reqId请求 ID对应发送 reqAccountSummary() 时传入的 reqId
account账户号敏感信息,公开日志里应脱敏
tag字段标签例如 NetLiquidationBuyingPower
value字段值字符串类型,金额计算时建议转成 Decimal
currency币种可能为空,例如 AccountTypeCushion

如果请求 5 个标签、这个会话可见 1 个账户,最少可能收到 5 次 accountSummary()。如果是多个账户,或 TWS 推送了更新,回调次数会更多。

accountSummaryEnd(reqId) 只表示“这一轮初始数据已经发完”。它不代表订阅已经自动取消,也不代表之后一定不会再有更新。

常见处理方式:

场景处理方式
只取一次账户摘要收到 accountSummaryEnd() 后整理结果,再调用 cancelAccountSummary(reqId)
持续显示账户摘要收到 accountSummaryEnd() 后继续保持连接,之后的 accountSummary() 更新仍按同样方式处理
多个请求同时存在reqId 分开保存不同请求的数据

官方账户摘要订阅的更新频率约为 3 分钟,不能由 API 客户端自行调快。需要更细的账户、持仓或组合变化时,应结合账户更新、持仓和组合相关接口。

最实用的结构是按账户、字段和币种保存最新值:

latest[(account, tag, currency)] = value

或者整理成嵌套字典:

summary[account][tag] = {
"value": value,
"currency": currency,
}

不要简单地把每一次回调都追加成一行长期保存。账户摘要是订阅式数据,同一个字段可能先返回初始值,再返回更新值。做界面展示和策略判断时,通常应使用最新值。

下面示例展示如何接收回调、判断 value 是否像数字、等待 accountSummaryEnd(),并把结果按字段去重。示例中的公开输出不会打印真实账户号和真实金额。

import threading
from collections import defaultdict
from decimal import Decimal, InvalidOperation
from ibapi.client import EClient
from ibapi.wrapper import EWrapper
REQ_ID = 9004
TAGS = "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=True
REQUEST_ID=9004
END_REQUEST_ID=9004
END_MATCHES_REQUEST=True
REQUESTED_TAGS=AccountType,NetLiquidation,BuyingPower,AvailableFunds,Cushion
RAW_CALLBACK_ROWS=10
UNIQUE_FIELD_ROWS=5
TAGS_RECEIVED=AccountType,AvailableFunds,BuyingPower,Cushion,NetLiquidation
CURRENCIES=EMPTY,USD
VALUE_TYPES=decimal,text
ACCOUNT_ALIAS=ACCOUNT_1;FIELD=AccountType;CURRENCY=EMPTY;VALUE_TYPE=text;VALUE=<redacted>;CALLBACK_COUNT=2
ACCOUNT_ALIAS=ACCOUNT_1;FIELD=AvailableFunds;CURRENCY=USD;VALUE_TYPE=decimal;VALUE=<redacted>;CALLBACK_COUNT=2
ACCOUNT_ALIAS=ACCOUNT_1;FIELD=BuyingPower;CURRENCY=USD;VALUE_TYPE=decimal;VALUE=<redacted>;CALLBACK_COUNT=2
ACCOUNT_ALIAS=ACCOUNT_1;FIELD=Cushion;CURRENCY=EMPTY;VALUE_TYPE=decimal;VALUE=<redacted>;CALLBACK_COUNT=2
ACCOUNT_ALIAS=ACCOUNT_1;FIELD=NetLiquidation;CURRENCY=USD;VALUE_TYPE=decimal;VALUE=<redacted>;CALLBACK_COUNT=2
NON_INFO_ERROR_COUNT=0
IS_CONNECTED_AFTER_DISCONNECT=False

这次请求了 5 个字段,实际收到了 10 次原始回调,去重后是 5 个字段。账户摘要是订阅型数据,同一个字段可能先返回初始值,再返回更新值。程序应按 (account, tag, currency) 保存最新值,不应把重复回调当成异常。

value 在官方回调里是字符串。处理方式可以按字段类型区分:

字段类型示例建议
文本字段AccountType保留字符串
金额字段NetLiquidationAvailableFundsDecimal(value) 转换
比例字段CushionLeverageDecimal(value) 转换,并明确显示口径
时间字段LookAheadNextChange按官方返回格式解析,不要当金额处理

财务金额不建议直接使用浮点数长期计算。浮点数适合临时展示,不适合做严肃对账或风控判断。

账户摘要里包含账户号、现金、净值、保证金和购买力,公开环境必须谨慎处理:

数据公开展示建议
account替换成 ACCOUNT_1ACCOUNT_2
value金额和权益值替换成 <redacted>
currency可保留,例如 USD
tag可保留,字段名本身不敏感
错误码可保留,但不要附带账户资产明细

如果用户需要排查问题,优先提供字段名、请求 ID、错误码、是否收到 accountSummaryEnd(),不要直接贴完整账户摘要。

现象常见原因处理方式
回调行数比字段数多初始快照和更新都触发了 accountSummary()按账户、标签和币种保存最新值
没收到 accountSummaryEnd()TWS 未允许 API、请求参数错误、连接断开检查端口、API 设置、groupNametags
END_REQUEST_ID 和请求 ID 不一致多个请求混在一起处理,或变量使用错误reqId 分桶保存每个请求的数据
value 转数字失败字段是文本,或返回值不是标准数字格式按字段类型转换,不要统一强转
币种为空字段本身不是金额字段允许空币种,展示为 EMPTY 或空字符串

IBKR Campus: TWS API Documentation