跳转到内容

接收按模型返回的账户更新

发起 reqAccountUpdatesMulti() 后,账户字段会通过 accountUpdateMulti() 一行一行返回。初始批次结束时,TWS 会调用 accountUpdateMultiEnd(reqId)

accountUpdateMulti(reqId, account, modelCode, key, value, currency)
accountUpdateMultiEnd(reqId)

接收这组回调时,不要只看 key。multi 接口的价值在于同时带回 reqIdaccountmodelCode,程序可以用它们把不同请求、不同账户和不同模型组合的数据分开。

def accountUpdateMulti(self, reqId, account, modelCode, key, value, currency):
...
字段中文含义处理建议
reqId请求编号先用它过滤回调,避免不同请求的数据混在一起。
account账户代码程序可以保存,公开日志和报错内容应脱敏。
modelCode模型组合代码空字符串表示没有按模型组合返回;有模型组合时用它分组。
key字段名决定这行数据代表什么,例如现金、净值、账本字段。
value字段值字符串形式返回,按字段用途再转换成数字或保留文本。
currency币种可能是 USDBASE,也可能为空。

value 虽然常常看起来像数字,但 API 层返回的是字符串。比较稳妥的做法是:先按 key 建立字段字典,再对确定是金额、比例、数量的字段做数值转换。

def accountUpdateMultiEnd(self, reqId):
...

accountUpdateMultiEnd(reqId) 表示这个 reqId 对应的初始账户字段批次已经返回完。它不携带账户号、模型代码或字段值,所以程序应该在 accountUpdateMulti() 中自己收集数据,再在结束回调里触发汇总处理。

常见处理方式:

  • 收到目标 reqId 的字段行后存入列表或字典。
  • 收到 accountUpdateMultiEnd(reqId) 后,把这批数据标记为可用。
  • 页面展示或策略读取时,从已收集的数据结构里按 keycurrency 取值。

如果你只按 key 存储数据,多币种账户可能会覆盖值。更稳妥的键可以包含账户、模型、字段和币种:

store_key = (
account,
modelCode or "",
key,
currency or "",
)
account_values[store_key] = value

这样 $LEDGER-CashBalanceBASE 行和 USD 行不会互相覆盖。

下面示例会收集所有 accountUpdateMulti() 回调,并在结束后统计 reqIdmodelCode、币种、字段数量和字段值类型。真实账户号和金额不会打印。

import threading
import time
from collections import Counter
from ibapi.client import EClient
from ibapi.wrapper import EWrapper
REQ_ID = 9101
INFO_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=True
END_REQID_MATCHES=True
RAW_CALLBACK_ROWS=50
UNIQUE_KEY_COUNT=25
MODEL_CODE_COUNTS=EMPTY:50
CURRENCY_COUNTS=BASE:25,USD:25
VALUE_TYPE_COUNTS=decimal:44,text:6
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
INFO_CODES=2104,2106,2158
NON_INFO_ERROR_COUNT=0

这次返回的 50 行数据由 25 个字段 key 组成,每个 key 分别返回 BASEUSD 两个币种行。44 行字段值可以转换成数字,6 行是文本字段。

本例样例 key 都带 $LEDGER- 前缀,表示账本相关字段。常见含义如下:

key中文含义说明
$LEDGER-CashBalance现金余额按币种返回现金余额
$LEDGER-AccruedCash应计现金可能包含利息等应计项目
$LEDGER-FxCashBalance外汇现金余额多币种账户常见
$LEDGER-FuturesPNL期货盈亏与期货相关账户账本字段
$LEDGER-ExchangeRate汇率币种折算相关
$LEDGER-AccountOrGroup账户或分组文本字段,不要转成数字
$LEDGER-Currency币种文本字段,通常与 currency 列一起理解

完整账户字段很多,写程序时不要只靠字段顺序。字段顺序可能随账户状态、权限、币种和 TWS 版本变化,应按 key 取值。

账户更新回调可能包含账户号、资金、保证金、现金余额、净值、币种和模型组合信息。公开日志建议只输出:

  • reqId
  • 字段数量
  • 去重 key 数量
  • 币种分布
  • 字段名样例
  • 非信息类错误数量

不要在页面、日志或报错里直接显示真实账户号、真实金额、持仓成本和盈亏。

IBKR Campus: TWS API Documentation