接收持仓
reqPositions() 的持仓行通过 position() 回调返回。每个持仓合约一行。初始批次结束时,TWS 调用 positionEnd()。
def position(self, account, contract, position, avgCost): ...
def positionEnd(self): ...position() 字段
Section titled “position() 字段”| 字段 | 中文含义 | 说明 |
|---|---|---|
account | 账户代码 | 真实账户号,公开输出必须脱敏 |
contract | 合约对象 | 包含合约 ID、代码、证券类型、交易所、币种等 |
position | 持仓数量 | 可能是小数类型,不要只按整数处理 |
avgCost | 平均成本 | 敏感字段,公开日志不要直接输出 |
常用 contract 字段:
| 字段 | 中文含义 | 示例 |
|---|---|---|
contract.conId | 合约 ID | 265598 |
contract.symbol | 标的代码 | AAPL |
contract.secType | 证券类型 | STK、OPT、FUT |
contract.exchange | 交易所或路由 | SMART |
contract.currency | 币种 | USD |
positionEnd() 的含义
Section titled “positionEnd() 的含义”positionEnd() 表示初始持仓批次已经返回完。它不是取消订阅,也不代表之后一定不会有更新。若只需要快照,收到它之后再调用 cancelPositions()。
如果 positionEnd() 到达但没有任何 position() 行,通常说明账户没有持仓。
Python 接收示例
Section titled “Python 接收示例”import threadingfrom collections import Counterfrom ibapi.client import EClientfrom ibapi.wrapper import EWrapper
INFO_CODES = {2100, 2104, 2106, 2158}
class ReceivePositionsApp(EWrapper, EClient): def __init__(self): EClient.__init__(self, self) self.ready = threading.Event() self.positions_end = threading.Event() self.positions = [] self.info_codes = [] self.errors_seen = []
def nextValidId(self, orderId): self.ready.set()
def position(self, account, contract, position, avgCost): self.positions.append({ "account": account, "conId": contract.conId, "symbol": contract.symbol, "secType": contract.secType, "currency": contract.currency, })
def positionEnd(self): 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 = ReceivePositionsApp()
try: app.connect("127.0.0.1", 7497, clientId=986)
thread = threading.Thread(target=app.run, daemon=True) thread.start()
if not app.ready.wait(8): raise RuntimeError("等待 nextValidId 超时")
app.reqPositions()
if not app.positions_end.wait(8): raise RuntimeError("等待 positionEnd 超时")
finally: if app.isConnected(): app.cancelPositions() app.disconnect()
sec_type_counts = Counter(row["secType"] or "EMPTY" for row in app.positions)currency_counts = Counter(row["currency"] or "EMPTY" for row in app.positions)
print(f"POSITION_END_RECEIVED={app.positions_end.is_set()}")print(f"POSITION_ROW_COUNT={len(app.positions)}")print("SEC_TYPE_COUNTS=" + (",".join(f"{key}:{value}" for key, value in sec_type_counts.items()) if sec_type_counts else "NONE"))print("CURRENCY_COUNTS=" + (",".join(f"{key}:{value}" for key, value in currency_counts.items()) if currency_counts else "NONE"))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)}")POSITION_END_RECEIVED=TruePOSITION_ROW_COUNT=0SEC_TYPE_COUNTS=NONECURRENCY_COUNTS=NONEINFO_CODES=2104,2106,2158NON_INFO_ERROR_COUNT=0账户没有持仓行,所以证券类型和币种分布都是 NONE。
如何处理真实持仓
Section titled “如何处理真实持仓”如果你的账户有持仓,建议用结构化对象保存,不要直接拼字符串:
row = { "account": account, "conId": contract.conId, "symbol": contract.symbol, "secType": contract.secType, "currency": contract.currency, "position": str(position), "avgCost": avgCost,}公开日志中可以只保留:
safe_row = { "account": "ACCOUNT_1", "conId": contract.conId, "secType": contract.secType, "currency": contract.currency,}position 为什么要转字符串?
Section titled “position 为什么要转字符串?”Python API 里持仓数量可能是 Decimal 类型。写 JSON、日志或数据库前,转成字符串能避免精度和序列化问题。
avgCost 可以公开吗?
Section titled “avgCost 可以公开吗?”不建议。平均成本能暴露交易成本和仓位信息,公开示例里应隐藏或只统计。
positionEnd() 之后还会有更新吗?
Section titled “positionEnd() 之后还会有更新吗?”可能。positionEnd() 只表示初始批次结束。订阅还存在时,之后的持仓变化可能继续通过 position() 返回。