跳转到内容

按模型持仓总览

按模型持仓接口用于按“账户”和“模型组合”过滤持仓。它比普通 reqPositions() 更精确,适合多账户、顾问账户、模型组合账户,或者希望每个请求都有独立编号的程序。

app.reqPositionsMulti(reqId, account, modelCode)

返回数据通过两个回调接收:

def positionMulti(self, reqId, account, modelCode, contract, pos, avgCost):
...
def positionMultiEnd(self, reqId):
...

取消订阅时使用同一个 reqId

app.cancelPositionsMulti(reqId)
接口适合场景是否有请求编号是否可指定账户是否可指定模型
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)

下面示例不写死账户号,而是先通过 reqManagedAccts() 获取这个 TWS 会话可见账户,再请求该账户的全部模型持仓。

import threading
import time
from collections import Counter
from ibapi.client import EClient
from 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=True
REQUEST_ID=9201
ACCOUNT_ALIAS=ACCOUNT_1
MODEL_CODE=EMPTY
MANAGED_ACCOUNT_COUNT=1
REQUEST_SENT=True
POSITION_MULTI_END_RECEIVED=True
END_REQID_MATCHES=True
POSITION_MULTI_ROW_COUNT=0
REQID_COUNTS=NONE
MODEL_CODE_COUNTS=NONE
SEC_TYPE_COUNTS=NONE
CURRENCY_COUNTS=NONE
CANCEL_SENT=True
ROWS_BEFORE_CANCEL=0
ROWS_AFTER_CANCEL_WAIT=0
INFO_CODES=2104,2106,2158
NON_INFO_ERROR_COUNT=0
IS_CONNECTED_AFTER_DISCONNECT=False

TWS 模拟账户没有持仓,所以没有 positionMulti() 行;但收到了 positionMultiEnd(),并且没有非信息类错误,说明请求、结束回调和取消流程都是通的。

可以用。没有模型组合时,modelCode 传空字符串 "",它会按账户维度返回持仓。

文档示例不建议写死账户号。先用 reqManagedAccts() 获取这个会话可见账户,可以减少用户复制代码后因为账户号不一致导致失败。

不是。判断这类请求是否完成,重点看是否收到 positionMultiEnd(reqId)。没有持仓时,返回 0 行是正常结果。