按模型持仓总览
按模型持仓接口用于按“账户”和“模型组合”过滤持仓。它比普通 reqPositions() 更精确,适合多账户、顾问账户、模型组合账户,或者希望每个请求都有独立编号的程序。
app.reqPositionsMulti(reqId, account, modelCode)返回数据通过两个回调接收:
def positionMulti(self, reqId, account, modelCode, contract, pos, avgCost): ...
def positionMultiEnd(self, reqId): ...取消订阅时使用同一个 reqId:
app.cancelPositionsMulti(reqId)和普通持仓接口的区别
Section titled “和普通持仓接口的区别”| 接口 | 适合场景 | 是否有请求编号 | 是否可指定账户 | 是否可指定模型 |
|---|---|---|---|---|
reqPositions() | 简单账户、读取全部可见持仓 | 否 | 否 | 否 |
reqPositionsMulti() | 多账户、模型组合、需要区分多个请求 | 是 | 是 | 是 |
如果只是读取登录账户的全部持仓,reqPositions() 更简单。如果你的程序需要明确知道“这批持仓来自哪个账户、哪个模型”,应该使用 reqPositionsMulti()。
| 参数 | 中文含义 | 说明 |
|---|---|---|
reqId | 请求编号 | 由程序自己分配,返回和取消时都用它识别这次请求 |
account | 账户代码 | 可传具体账户;多账户环境下建议先用 reqManagedAccts() 获取 |
modelCode | 模型代码 | 没有模型过滤时传空字符串 "" |
modelCode 不是股票代码,也不是策略名称。它对应 TWS / IBKR 账户中的模型组合代码。普通个人模拟账户通常没有模型组合,因此常见传法是空字符串。
连接 TWS / IB Gateway -> 等待 nextValidId() -> reqManagedAccts() -> 选择账户 -> reqPositionsMulti(reqId, account, "") -> positionMulti(...) -> positionMultiEnd(reqId) -> cancelPositionsMulti(reqId)positionMultiEnd(reqId) 表示这次请求的初始持仓批次已经返回完成。它不是取消订阅,若只需要一次快照,仍建议随后调用 cancelPositionsMulti(reqId)。
Python 示例
Section titled “Python 示例”下面示例不写死账户号,而是先通过 reqManagedAccts() 获取这个 TWS 会话可见账户,再请求该账户的全部模型持仓。
import threadingimport timefrom collections import Counter
from ibapi.client import EClientfrom ibapi.wrapper import EWrapper
INFO_CODES = {2100, 2103, 2104, 2105, 2106, 2107, 2108, 2158}
class PositionsMultiApp(EWrapper, EClient): def __init__(self): EClient.__init__(self, self) self.ready = threading.Event() self.accounts_ready = threading.Event() self.positions_end = threading.Event() self.accounts = [] self.positions = [] self.end_req_id = None self.info_codes = [] self.errors_seen = []
def nextValidId(self, orderId): self.ready.set()
def managedAccounts(self, accountsList): self.accounts = [item for item in accountsList.split(",") if item] self.accounts_ready.set()
def positionMulti(self, reqId, account, modelCode, contract, pos, avgCost): # 真实账户号、持仓数量和平均成本都敏感;公开输出只统计结构。 self.positions.append({ "reqId": reqId, "modelCode": modelCode or "EMPTY", "secType": contract.secType or "EMPTY", "currency": contract.currency or "EMPTY", })
def positionMultiEnd(self, reqId): self.end_req_id = reqId self.positions_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 = PositionsMultiApp()req_id = 9201
try: app.connect("127.0.0.1", 7497, clientId=118) 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.accounts_ready.wait(5): raise RuntimeError("等待 managedAccounts 超时")
account = app.accounts[0] app.reqPositionsMulti(req_id, account, "")
if not app.positions_end.wait(8): raise RuntimeError("等待 positionMultiEnd 超时")
rows_before_cancel = len(app.positions)
app.cancelPositionsMulti(req_id) time.sleep(0.5) rows_after_cancel = len(app.positions)
finally: if app.isConnected(): app.disconnect()
reqid_counts = Counter(row["reqId"] for row in app.positions)model_counts = Counter(row["modelCode"] for row in app.positions)sec_type_counts = Counter(row["secType"] for row in app.positions)currency_counts = Counter(row["currency"] for row in app.positions)
print(f"CONNECTED={connected}")print(f"REQUEST_ID={req_id}")print("ACCOUNT_ALIAS=ACCOUNT_1")print("MODEL_CODE=EMPTY")print(f"MANAGED_ACCOUNT_COUNT={len(app.accounts)}")print("REQUEST_SENT=True")print(f"POSITION_MULTI_END_RECEIVED={app.positions_end.is_set()}")print(f"END_REQID_MATCHES={app.end_req_id == req_id}")print(f"POSITION_MULTI_ROW_COUNT={len(app.positions)}")print("REQID_COUNTS=" + (",".join(f"{k}:{v}" for k, v in reqid_counts.items()) if reqid_counts else "NONE"))print("MODEL_CODE_COUNTS=" + (",".join(f"{k}:{v}" for k, v in model_counts.items()) if model_counts else "NONE"))print("SEC_TYPE_COUNTS=" + (",".join(f"{k}:{v}" for k, v in sec_type_counts.items()) if sec_type_counts else "NONE"))print("CURRENCY_COUNTS=" + (",".join(f"{k}:{v}" for k, v in currency_counts.items()) if currency_counts else "NONE"))print("CANCEL_SENT=True")print(f"ROWS_BEFORE_CANCEL={rows_before_cancel}")print(f"ROWS_AFTER_CANCEL_WAIT={rows_after_cancel}")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)}")print(f"IS_CONNECTED_AFTER_DISCONNECT={app.isConnected()}")CONNECTED=TrueREQUEST_ID=9201ACCOUNT_ALIAS=ACCOUNT_1MODEL_CODE=EMPTYMANAGED_ACCOUNT_COUNT=1REQUEST_SENT=TruePOSITION_MULTI_END_RECEIVED=TrueEND_REQID_MATCHES=TruePOSITION_MULTI_ROW_COUNT=0REQID_COUNTS=NONEMODEL_CODE_COUNTS=NONESEC_TYPE_COUNTS=NONECURRENCY_COUNTS=NONECANCEL_SENT=TrueROWS_BEFORE_CANCEL=0ROWS_AFTER_CANCEL_WAIT=0INFO_CODES=2104,2106,2158NON_INFO_ERROR_COUNT=0IS_CONNECTED_AFTER_DISCONNECT=FalseTWS 模拟账户没有持仓,所以没有 positionMulti() 行;但收到了 positionMultiEnd(),并且没有非信息类错误,说明请求、结束回调和取消流程都是通的。
没有模型代码是不是不能用?
Section titled “没有模型代码是不是不能用?”可以用。没有模型组合时,modelCode 传空字符串 "",它会按账户维度返回持仓。
为什么要先拿账户列表?
Section titled “为什么要先拿账户列表?”文档示例不建议写死账户号。先用 reqManagedAccts() 获取这个会话可见账户,可以减少用户复制代码后因为账户号不一致导致失败。
0 行持仓是不是失败?
Section titled “0 行持仓是不是失败?”不是。判断这类请求是否完成,重点看是否收到 positionMultiEnd(reqId)。没有持仓时,返回 0 行是正常结果。