接收按模型返回的账户更新
发起 reqAccountUpdatesMulti() 后,账户字段会通过 accountUpdateMulti() 一行一行返回。初始批次结束时,TWS 会调用 accountUpdateMultiEnd(reqId)。
accountUpdateMulti(reqId, account, modelCode, key, value, currency)accountUpdateMultiEnd(reqId)接收这组回调时,不要只看 key。multi 接口的价值在于同时带回 reqId、account 和 modelCode,程序可以用它们把不同请求、不同账户和不同模型组合的数据分开。
def accountUpdateMulti(self, reqId, account, modelCode, key, value, currency): ...| 字段 | 中文含义 | 处理建议 |
|---|---|---|
reqId | 请求编号 | 先用它过滤回调,避免不同请求的数据混在一起。 |
account | 账户代码 | 程序可以保存,公开日志和报错内容应脱敏。 |
modelCode | 模型组合代码 | 空字符串表示没有按模型组合返回;有模型组合时用它分组。 |
key | 字段名 | 决定这行数据代表什么,例如现金、净值、账本字段。 |
value | 字段值 | 字符串形式返回,按字段用途再转换成数字或保留文本。 |
currency | 币种 | 可能是 USD、BASE,也可能为空。 |
value 虽然常常看起来像数字,但 API 层返回的是字符串。比较稳妥的做法是:先按 key 建立字段字典,再对确定是金额、比例、数量的字段做数值转换。
def accountUpdateMultiEnd(self, reqId): ...accountUpdateMultiEnd(reqId) 表示这个 reqId 对应的初始账户字段批次已经返回完。它不携带账户号、模型代码或字段值,所以程序应该在 accountUpdateMulti() 中自己收集数据,再在结束回调里触发汇总处理。
常见处理方式:
- 收到目标
reqId的字段行后存入列表或字典。 - 收到
accountUpdateMultiEnd(reqId)后,把这批数据标记为可用。 - 页面展示或策略读取时,从已收集的数据结构里按
key和currency取值。
字段组织方式
Section titled “字段组织方式”如果你只按 key 存储数据,多币种账户可能会覆盖值。更稳妥的键可以包含账户、模型、字段和币种:
store_key = ( account, modelCode or "", key, currency or "",)account_values[store_key] = value这样 $LEDGER-CashBalance 的 BASE 行和 USD 行不会互相覆盖。
Python 接收示例
Section titled “Python 接收示例”下面示例会收集所有 accountUpdateMulti() 回调,并在结束后统计 reqId、modelCode、币种、字段数量和字段值类型。真实账户号和金额不会打印。
import threadingimport timefrom collections import Counterfrom ibapi.client import EClientfrom ibapi.wrapper import EWrapper
REQ_ID = 9101INFO_CODES = {2100, 2104, 2106, 2158}
class ReceiveAccountUpdatesMultiApp(EWrapper, EClient): def __init__(self): EClient.__init__(self, self) self.ready = threading.Event() self.managed_ready = threading.Event() self.multi_end = threading.Event() self.managed_accounts = [] self.rows = [] self.end_req_id = 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 accountUpdateMulti(self, reqId, account, modelCode, key, value, currency): if reqId != REQ_ID: return
self.rows.append({ "account": account, "modelCode": modelCode or "EMPTY", "key": key, "value": value, "currency": currency or "EMPTY", })
def accountUpdateMultiEnd(self, reqId): self.end_req_id = reqId self.multi_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))
def value_type(value): try: float(value) return "decimal" except (TypeError, ValueError): return "text"
app = ReceiveAccountUpdatesMultiApp()
try: app.connect("127.0.0.1", 7497, clientId=976)
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.reqAccountUpdatesMulti(REQ_ID, account, "", True)
if not app.multi_end.wait(8): raise RuntimeError("等待 accountUpdateMultiEnd 超时")
app.cancelAccountUpdatesMulti(REQ_ID) time.sleep(0.5)
finally: if app.isConnected(): app.disconnect()
currency_counts = Counter(row["currency"] for row in app.rows)model_counts = Counter(row["modelCode"] for row in app.rows)value_type_counts = Counter(value_type(row["value"]) for row in app.rows)sample_keys = sorted({row["key"] for row in app.rows})[:14]
print(f"END_RECEIVED={app.multi_end.is_set()}")print(f"END_REQID_MATCHES={app.end_req_id == REQ_ID}")print(f"RAW_CALLBACK_ROWS={len(app.rows)}")print(f"UNIQUE_KEY_COUNT={len({row['key'] for row in app.rows})}")print("MODEL_CODE_COUNTS=" + ",".join(f"{key}:{value}" for key, value in model_counts.items()))print("CURRENCY_COUNTS=" + ",".join(f"{key}:{value}" for key, value in currency_counts.items()))print("VALUE_TYPE_COUNTS=" + ",".join(f"{key}:{value}" for key, value in value_type_counts.items()))print("SAMPLE_KEYS=" + ",".join(sample_keys))print("INFO_CODES=" + (",".join(map(str, sorted(set(app.info_codes)))) if app.info_codes else "NONE"))print(f"NON_INFO_ERROR_COUNT={len(app.errors_seen)}")脱敏后的参考输出如下:
END_RECEIVED=TrueEND_REQID_MATCHES=TrueRAW_CALLBACK_ROWS=50UNIQUE_KEY_COUNT=25MODEL_CODE_COUNTS=EMPTY:50CURRENCY_COUNTS=BASE:25,USD:25VALUE_TYPE_COUNTS=decimal:44,text:6SAMPLE_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-MutualFundValueINFO_CODES=2104,2106,2158NON_INFO_ERROR_COUNT=0这次返回的 50 行数据由 25 个字段 key 组成,每个 key 分别返回 BASE 和 USD 两个币种行。44 行字段值可以转换成数字,6 行是文本字段。
常见 key 怎么看
Section titled “常见 key 怎么看”本例样例 key 都带 $LEDGER- 前缀,表示账本相关字段。常见含义如下:
| key | 中文含义 | 说明 |
|---|---|---|
$LEDGER-CashBalance | 现金余额 | 按币种返回现金余额 |
$LEDGER-AccruedCash | 应计现金 | 可能包含利息等应计项目 |
$LEDGER-FxCashBalance | 外汇现金余额 | 多币种账户常见 |
$LEDGER-FuturesPNL | 期货盈亏 | 与期货相关账户账本字段 |
$LEDGER-ExchangeRate | 汇率 | 币种折算相关 |
$LEDGER-AccountOrGroup | 账户或分组 | 文本字段,不要转成数字 |
$LEDGER-Currency | 币种 | 文本字段,通常与 currency 列一起理解 |
完整账户字段很多,写程序时不要只靠字段顺序。字段顺序可能随账户状态、权限、币种和 TWS 版本变化,应按 key 取值。
数据脱敏建议
Section titled “数据脱敏建议”账户更新回调可能包含账户号、资金、保证金、现金余额、净值、币种和模型组合信息。公开日志建议只输出:
reqId- 字段数量
- 去重 key 数量
- 币种分布
- 字段名样例
- 非信息类错误数量
不要在页面、日志或报错里直接显示真实账户号、真实金额、持仓成本和盈亏。