ib_insync 进阶 — 连接、下单、查询、事件
ib_insync 同步/异步模型、Contract 合约限定、订单类型矩阵、账户/持仓查询、事件驱动模型、错误码
日期: 2026-05-11 方向: 个人量化交易 / IBKR API 阶段: Phase 1: 基础与工具链 标签: #IBKR #ib_insync #Contract #OrderTypes #EventHandlers #ErrorCodes
今日目标
| 类型 | 内容 |
|---|---|
| 学习 | ib_insync 同步/异步模型、Contract 合约限定、订单类型矩阵、账户/持仓查询、事件驱动模型、错误码 |
| 实操 | 写一个完整的「连接 → 限价下单 → 取消 → 验证状态 → 断开」往返脚本,带异常处理与事件回调 |
| 产出 | TR-DAY2 笔记 + 可直接运行的下单往返脚本 + 错误码速查表 |
一、ib_insync 同步 vs 异步:搞清楚再写代码
1.1 ib_insync 不是「同步库」也不是「异步库」,它是「事件循环上的同步外壳」
很多人以为 ib_insync 是同步 API,其实底层完全是 asyncio 驱动。它做的事情是:把每个看似阻塞的方法(reqMktData、placeOrder)内部 spin 一个事件循环,直到结果回来再返回。这意味着:
| 看起来 | 实际上 |
|---|---|
ib.connect(...) 阻塞返回 | 内部 loop.run_until_complete() |
ib.qualifyContracts(c) 同步返回 | 等待 server 回包,期间事件循环还在跑 |
ib.sleep(2) | 不是 time.sleep,是 loop.run_until_complete(asyncio.sleep(2)) |
核心后果:在 ib_insync 里绝对不能用 time.sleep——那会真正阻塞事件循环,导致回调收不到、连接被服务端踢掉。要等待用 ib.sleep(n)。
1.2 util.startLoop() vs ib.run():什么场景用什么
| 运行环境 | 调用方式 | 说明 |
|---|---|---|
| Jupyter / IPython | util.startLoop() 一次,然后正常写同步风格代码 | 因为 Jupyter 自己有 IPython kernel 的事件循环,需要 nest_asyncio 嵌套 |
| Python 脚本(standalone) | 不用 startLoop,直接同步代码即可 | ib_insync 自动启动 asyncio 循环 |
| 服务化长跑 | ib.run() 进入 loop.run_forever() | 让事件循环常驻,靠 event handler 推动业务 |
| 混入已有 asyncio 项目 | await ib.connectAsync(...) 等 async 方法 | 所有 IB 方法都有 *Async 版本 |
给我们的建议:
- 学习/开发:Jupyter +
util.startLoop()。能看图能 print。 - 回测脚本:standalone Python 脚本,不用任何特殊处理。
- Phase 3 自动化:standalone 脚本 +
ib.run(),靠orderStatusEvent推动状态机,配合 cron / systemd 跑。
1.3 什么时候必须切到 async 写法
90% 的个人量化场景不需要写 async。但下面这两类要:
- 同时连接多个 IB Gateway(分账户、分市场)。多个 IB 实例的 sleep/wait 互相干扰,必须 async 才能并发。
- 跟其他 async 框架共生(FastAPI / aiohttp 后端)。同步阻塞会卡住整个 Web server。
我们 90 天前 60 天都用同步写法,第 70 天起做 multi-account 时再切 async。
二、Contract 对象:金融实体的精确寻址
2.1 为什么 Contract 比股票代码复杂
「AAPL」这个字符串本身根本不能下单——同一个 ticker 可能在 NASDAQ、NYSE Arca、IEX 多个交易所交易;同一个标的可能有多个货币标价(BABA 在美/港);期权要再加上行权价、到期日、Call/Put、Multiplier。IBKR 需要你提供一个「唯一可识别」的 Contract 对象。
2.2 五种主要 Contract 类型
from ib_insync import Stock, Option, Future, Forex, Index, Bond
# 1) 股票 — 最常用
spy = Stock('SPY', 'SMART', 'USD')
# symbol exchange currency
# SMART = IBKR 智能路由(自动选最优交易所)
# ARCA / NASDAQ / NYSE = 指定交易所
# 2) 期权 — symbol + lastTradeDateOrContractMonth + strike + right + exchange
spy_call = Option('SPY', '20260619', 580, 'C', 'SMART', tradingClass='SPY')
# right: 'C' = Call, 'P' = Put
# tradingClass 在 SPX/SPXW 这种有「周/月」之分的合约上必填
# 3) 期货 — symbol + lastTradeDateOrContractMonth + exchange
es_dec = Future('ES', '202612', 'CME')
# 季月合约:H=3, M=6, U=9, Z=12
# 4) 外汇 — pair + exchange
eurusd = Forex('EURUSD', 'IDEALPRO')
# IDEALPRO 是 IBKR 的 FX 流动性池
# 5) 指数 — 用于查指数报价(不能直接交易,要交易得用期权或期货)
spx = Index('SPX', 'CBOE', 'USD')
2.3 qualifyContracts 为什么不是可选的
contract = Stock('AAPL', 'SMART', 'USD')
ib.qualifyContracts(contract)
# 调用之后 contract.conId 会被填充(IBKR 内部唯一 ID)
print(contract.conId) # 例如 265598
为什么必须:
- 歧义消解:同样写
Stock('BABA', 'SMART', 'USD')时,IBKR 需要在美 ADR 和港股 ADR 间选;qualify 会让它确认或报错。 - conId 是订单引擎真正用的:所有下单/查报价/查持仓内部都按 conId 索引。没 qualify 过的 contract 在某些方法里能跑,但返回数据可能错位。
- 失败 fail-fast:写错 ticker / 选错交易所 / 标的下市,qualify 直接抛异常,不会等到下单时才炸。
真实坑:Stock('GOOGL', 'SMART', 'USD') 和 Stock('GOOG', 'SMART', 'USD') 是不同 Class(A 类 vs C 类),千万别想当然。SPX 的「monthly」和「weekly」期权 tradingClass 也不同(SPX vs SPXW),不 qualify 会拿到错的 conId。
2.4 多结果时怎么办
某些查询会返回多个候选合约(典型场景:用 reqContractDetails 查所有可用期权链):
from ib_insync import Option
# 模糊匹配 — 不指定 strike 和 right,会返回完整链
template = Option('SPY', '20260619', 0, '', 'SMART')
details = ib.reqContractDetails(template)
print(f"找到 {len(details)} 个合约")
# details[i].contract 是完整 qualified contract
不要试图自己 join symbol + date + strike 拼合约——reqContractDetails 是规范方式。
三、订单类型:表面相似,行为差距巨大
3.1 七种核心订单类型对比
| 订单类型 | OrderType 字段 | 行为 | 适用场景 | 注意 |
|---|---|---|---|---|
| Market (MKT) | MKT | 立即按市价成交 | 必须立即成交(极少用) | 流动性差时滑点剧烈,期权基本禁用 |
| Limit (LMT) | LMT + lmtPrice | 不优于限价不成交 | 大部分量化下单 | 可能不成交,要监控状态 |
| Stop (STP) | STP + auxPrice | 触发后变 Market | 止损单 | 触发后滑点不可控 |
| Stop-Limit (STP LMT) | STP LMT + auxPrice + lmtPrice | 触发后挂限价 | 止损但限制最大滑点 | 极端行情可能不成交导致止损失效 |
| Trailing Stop (TRAIL) | TRAIL + trailingPercent 或 auxPrice | 价格反向走 X 后触发 | 锁利 + 让利润奔跑 | IBKR 服务端跟踪,不需要本地保活 |
| Bracket | 由 3 个 child order 组成 | 父单成交后自动挂 take-profit + stop-loss | 入场即定 R/R | 一次下单即出场逻辑确定 |
| Adaptive (IBALGO) | MKT + algoStrategy='Adaptive' | IBKR 路由优化滑点 | 中等单量、可接受秒级延迟 | 比纯 MKT 滑点小,比 LMT 成交率高 |
3.2 ib_insync 里的 Order 对象
from ib_insync import LimitOrder, MarketOrder, StopOrder, StopLimitOrder
# 限价买 SPY 100 股
o1 = LimitOrder('BUY', 100, 580.00)
# 市价卖 100 股
o2 = MarketOrder('SELL', 100)
# 止损卖 — 触发价 575
o3 = StopOrder('SELL', 100, 575.00)
# 止损限价卖 — 触发 575 后挂 limit 574
o4 = StopLimitOrder('SELL', 100, 574.00, 575.00)
3.3 IBKR 特有的关键字段
| 字段 | 含义 | 我们的用法 |
|---|---|---|
orderRef | 自定义字符串标签(≤16字节) | 写策略名 + 交易ID(如 mom_20260511_001),方便对账 |
tif (Time-in-Force) | 订单生命周期 | 见下表 |
outsideRth | 是否盘前盘后能成交 | 量化默认 False,避免极端价 |
transmit | 是否立即提交 | Bracket 父单 True,子单 False(链式提交) |
account | 多账户时指定 | 默认账户能省,多账户必填 |
whatIf | 试单(不真下) | 计算 margin 影响 |
hidden | 隐藏单(仅大单 ECN 支持) | 我们用不到 |
3.4 Time-in-Force(TIF)速查
| TIF | 含义 | 何时用 |
|---|---|---|
DAY | 当日有效,收盘自动取消 | 默认值,绝大多数场景 |
GTC | 撤前有效(Good-Till-Cancelled) | 长期挂单(如套利远月期权) |
IOC | 立即成交否则撤(Immediate-or-Cancel) | 抓瞬时流动性,部分成交可接受 |
FOK | 全部立即成交否则撤(Fill-or-Kill) | 大单要么全成要么不成 |
OPG | 仅开盘集合竞价 | 想吃开盘价 |
GTD | 指定日期前有效 | 财报前挂单到财报日 |
坑:GTC 在 IBKR 默认最长 90 天会被服务端自动取消;想真正长挂要监控并续期。
3.5 Bracket Order:一次下单定下整个交易计划
from ib_insync import Stock
contract = Stock('SPY', 'SMART', 'USD')
ib.qualifyContracts(contract)
bracket = ib.bracketOrder(
action='BUY',
quantity=100,
limitPrice=580.00, # 入场限价
takeProfitPrice=590.00, # +$10 止盈
stopLossPrice=575.00, # -$5 止损
)
# bracket 是 [parent, takeProfit, stopLoss] 三个 Order
for o in bracket:
ib.placeOrder(contract, o)
ib_insync 自动设置好 parentId、transmit 字段。父单成交后子单自动激活并互为 OCO(One-Cancels-Other)—— 一旦止盈成交,止损自动撤;反之亦然。
为什么 Bracket 是个人量化的最佳实践:
- 入场即把出场逻辑交给服务端,断网/电脑死机不影响 stop。
- 强制你下单前想清 R/R,杜绝「看心情止损」。
- 撤单成本低,策略调整方便。
四、账户与持仓查询:四个方法各有用途
4.1 四个核心查询方法
| 方法 | 返回 | 推/拉 | 延迟特性 |
|---|---|---|---|
reqAccountSummary() | 账户级聚合(NetLiq, BuyingPower, 等) | 订阅 + 推送 | 启动时一次性快照 + 后续变化推送 |
reqPositions() | 所有持仓 list | 订阅 + 推送 | 启动一次推全量,之后增量 |
reqPnL(account) | 账户级 daily PnL | 推送 | 实时(盘中持续刷新) |
reqPnLSingle(account, conId) | 单标的 PnL | 推送 | 实时 |
reqOpenOrders() | 当前未成交订单 | 一次拉 | 调用时快照 |
reqAllOpenOrders() | 所有 client 的 open order | 一次拉 | 调用时快照 |
reqExecutions() | 历史成交回报 | 一次拉 | 当日 + 历史(默认仅当日) |
4.2 reqAccountSummary 返回的字段
AccountValue 里关键 tag:
| Tag | 含义 |
|---|---|
NetLiquidation | 净清算价值(含浮盈浮亏) |
TotalCashValue | 现金余额 |
BuyingPower | 可用购买力 |
GrossPositionValue | 持仓总市值 |
MaintMarginReq | 维持保证金 |
AvailableFunds | 可用资金 = NetLiq − 占用保证金 |
ExcessLiquidity | 超额流动性(接近 0 会被强平) |
account = ib.managedAccounts()[0]
ib.reqAccountSummary()
ib.sleep(1) # 让推送到达
netliq = next(v for v in ib.accountValues()
if v.tag == 'NetLiquidation' and v.account == account)
print(f"Net Liq: {netliq.value} {netliq.currency}")
4.3 reqPositions 返回结构
positions = ib.positions() # 同步 helper,内部 reqPositions + 等
for p in positions:
print(f"{p.contract.symbol}: {p.position} @ avg {p.avgCost:.2f}")
Position 里的 avgCost 对期权是单张成本(不是 ×100 后的合约总成本),算盈亏时记得乘 multiplier。这是个人量化做期权策略最常见的「数字对不上」原因。
4.4 为什么不能只用 reqOpenOrders:推 vs 拉的认知
reqOpenOrders() 是拉——只反映你调用那一刻的状态。如果在两次拉之间订单状态变了(成交/部分成交/被券商拒绝),你不知道。
正确模式:用 reqOpenOrders 做启动时的状态恢复,靠 orderStatusEvent 做实时更新。下面第五章详述。
五、Event Handlers:交易系统必须事件驱动
5.1 为什么 polling 会出事
想象一个场景:你下了限价单,每 2 秒 reqOpenOrders() 检查是否成交。在两次检查之间:
- 订单成交 → 你延迟 2 秒才知道。
- 服务端拒单(比如保证金不足)→ 错误回调你没监听 → 你以为还在挂单 → 决策基于错的状态。
- 你的下一个信号触发 → 你以为没仓位实际有仓位 → 重复下单。
金融系统里 polling 不仅慢,是错的——状态变化是事件,你必须订阅。
5.2 ib_insync 的核心事件
| 事件 | 触发时机 | 回调签名 |
|---|---|---|
ib.orderStatusEvent | 订单状态变化(提交/部分成交/全成交/取消/拒绝) | (trade: Trade) |
ib.execDetailsEvent | 实际成交回报(含成交价/手续费) | (trade, fill) |
ib.errorEvent | 服务端报错 | (reqId, errorCode, errorString, contract) |
ib.commissionReportEvent | 手续费回报 | (trade, fill, report) |
ib.positionEvent | 持仓变化 | (position) |
ib.pnlEvent | PnL 变化 | (pnl) |
ib.disconnectedEvent | 连接断开 | () |
ib.newOrderEvent | 新订单创建 | (trade) |
5.3 注册回调的两种写法
# 方式 1: += 操作符
def on_order_status(trade):
print(f"[STATUS] {trade.order.orderId} {trade.orderStatus.status} "
f"filled={trade.orderStatus.filled}/{trade.order.totalQuantity}")
ib.orderStatusEvent += on_order_status
# 方式 2: 装饰器(如果只关心一个事件)
@ib.errorEvent
def on_error(reqId, code, msg, contract):
print(f"[ERROR {code}] {msg}")
5.4 Trade 对象:状态机的载体
ib.placeOrder(contract, order) 返回一个 Trade 对象。这是个活对象——它的 orderStatus、fills、log 字段会随着事件自动更新,不用你手动 refresh。
trade = ib.placeOrder(contract, order)
print(trade.orderStatus.status) # 'PendingSubmit'
ib.sleep(1)
print(trade.orderStatus.status) # 'Submitted'
# ... 成交后
print(trade.orderStatus.status) # 'Filled'
print(trade.fills[0].execution.price, trade.fills[0].execution.shares)
5.5 OrderStatus 的状态枚举
PendingSubmit -- 客户端已发,服务端未确认
PendingCancel -- 取消请求已发
PreSubmitted -- 服务端已收,但还在做风控/路由
Submitted -- 已挂上交易所
ApiCancelled -- 客户端发起取消,已被取消
Cancelled -- 服务端取消(包括过期、撤单)
Filled -- 全部成交
Inactive -- 服务端拒绝/不接受(要看 errorEvent 知道为什么)
写状态机时:「Submitted」不代表会成交,「Inactive」要看错误码定原因,「Filled」时去看 fills 拿真实成交价(限价单可能成交在更优价)。
六、错误码速查:能省你 80% 的调试时间
6.1 错误码不是异常,是事件
ib_insync 不会对服务端错误抛 Python 异常——错误是通过 errorEvent 推送的。下面这些代码值得背:
| Code | 含义 | 严重度 | 处理 |
|---|---|---|---|
| 200 | 没有该合约 / 没数据 | 警告 | 检查 symbol/exchange |
| 201 | 订单被拒绝 | 错误 | 看附带 message:保证金不足/权限不够/价格异常 |
| 202 | 订单已取消 | 信息 | 正常事件,不算错 |
| 300 | 找不到 ticker ID | 警告 | 你查一个未订阅过的 reqId |
| 354 | 没订阅市场数据 | 错误 | 去 IBKR 买订阅 |
| 399 | 订单消息(不是错) | 信息 | 比如 "Your order will not be placed at the exchange until...";可忽略 |
| 502 | 连不上 TWS | 错误 | TWS 没开 / 端口错 / API 没启用 |
| 504 | 未连接 | 错误 | 你在 ib.disconnect() 后还在用 ib |
| 507 | 不识别的消息 | 错误 | 通常是 ib_insync 与 TWS 版本不匹配 |
| 1100 | 与 TWS 的连接断了 | 严重 | 准备重连 |
| 1101 | 连接恢复(数据丢失) | 警告 | 需要重新订阅市场数据 |
| 1102 | 连接恢复(数据完整) | 信息 | 可以继续 |
| 2104 | "Market data farm OK" | 信息 | 启动时正常 |
| 2106 | "HMDS data farm OK" | 信息 | 历史数据服务 OK |
| 2158 | "Sec-def data farm OK" | 信息 | 合约定义服务 OK |
| 10089 | 期权权限不足 | 错误 | Options Level 没批到该策略需要的级别 |
| 10147 | OrderId 不存在(取消时) | 错误 | 撤单的订单已不在队列 |
| 10148 | OrderId 不可取消(已成交/已取消) | 错误 | 状态先于撤单到达 |
6.2 如何分类处理
INFO_CODES = {2104, 2106, 2158, 2107, 2119, 399, 202}
RECOVERABLE = {1100, 1101, 1102}
FATAL = {502, 504, 507}
def on_error(reqId, code, msg, contract):
if code in INFO_CODES:
return # 静默
if code in RECOVERABLE:
print(f"[CONN] {code} {msg} — 准备重连")
# trigger reconnect
return
if code in FATAL:
raise RuntimeError(f"Fatal IBKR error {code}: {msg}")
print(f"[ERROR {code}] reqId={reqId} {msg}")
ib.errorEvent += on_error
七、第一个完整往返脚本
7.1 设计目标
写一个能直接 python tr_day2_roundtrip.py 跑的脚本,做这五件事:
- 连接 Paper(带端口校验)
- 注册 errorEvent / orderStatusEvent
- Qualify SPY 合约
- 下一个远离市价的限价买单(确保不会成交)
- 立即取消该订单
- 等待状态稳定,打印订单生命周期日志
- 清理后断开
7.2 完整代码
"""
tr_day2_roundtrip.py
完整的「下单 → 撤单」往返脚本,演示连接、合约、下单、事件、取消、状态校验。
运行前提:
- TWS Paper 在 7497 端口运行
- 已 enable Read-Only 解除(API → Settings 取消勾选 Read-Only API)
"""
from ib_insync import IB, Stock, LimitOrder, util
import sys
import time
# ---------- 安全护栏 ----------
PAPER_PORT = 7497 # TWS Paper
LIVE_PORT = 7496 # TWS Live — 任何脚本里都别碰这个
HOST = '127.0.0.1'
CLIENT_ID = 11
# ---------- 事件回调 ----------
INFO_CODES = {2104, 2106, 2107, 2119, 2158, 399, 202}
def install_event_handlers(ib: IB):
def on_error(reqId, code, msg, contract):
if code in INFO_CODES:
return
sym = contract.symbol if contract else '-'
print(f"[ERROR {code}] reqId={reqId} sym={sym} msg={msg}")
def on_order_status(trade):
s = trade.orderStatus
print(f"[STATUS] orderId={trade.order.orderId} "
f"status={s.status} filled={s.filled}/{trade.order.totalQuantity} "
f"avgFillPrice={s.avgFillPrice}")
def on_exec(trade, fill):
e = fill.execution
print(f"[EXEC] {e.side} {e.shares} {trade.contract.symbol} "
f"@ {e.price} execId={e.execId}")
def on_disconnect():
print("[CONN] disconnected")
ib.errorEvent += on_error
ib.orderStatusEvent += on_order_status
ib.execDetailsEvent += on_exec
ib.disconnectedEvent += on_disconnect
# ---------- 主流程 ----------
def main():
ib = IB()
install_event_handlers(ib)
# 1) 连接 + 端口校验
print(f"Connecting to {HOST}:{PAPER_PORT} clientId={CLIENT_ID} ...")
try:
ib.connect(HOST, PAPER_PORT, clientId=CLIENT_ID, timeout=10)
except Exception as e:
print(f"connect failed: {e}")
sys.exit(1)
assert ib.client.port == PAPER_PORT, \
f"WRONG PORT {ib.client.port} — refuse to run on non-paper"
print(f"Connected. server={ib.client.serverVersion()} "
f"account={ib.managedAccounts()}")
# 2) Qualify SPY
spy = Stock('SPY', 'SMART', 'USD')
qualified = ib.qualifyContracts(spy)
if not qualified:
print("qualify failed — check market data subscription")
ib.disconnect()
sys.exit(2)
print(f"SPY qualified: conId={spy.conId}")
# 3) 拿 snapshot 报价以决定一个安全(不会成交)的限价
ticker = ib.reqMktData(spy, '', snapshot=True, regulatorySnapshot=False)
ib.sleep(2)
ref = ticker.last if ticker.last and ticker.last > 0 else (ticker.close or 580.0)
safe_limit = round(ref * 0.5, 2) # 50% 折扣:绝对不会成交
print(f"SPY ref price: {ref}, safe_limit (won't fill): {safe_limit}")
# 4) 下限价买单 1 股
order = LimitOrder('BUY', 1, safe_limit)
order.tif = 'DAY'
order.orderRef = 'tr_day2_test_001'
order.outsideRth = False
trade = ib.placeOrder(spy, order)
print(f"Order placed: orderId={trade.order.orderId}")
# 5) 等订单状态稳定到 Submitted / PreSubmitted
deadline = time.time() + 10
while trade.orderStatus.status not in ('Submitted', 'PreSubmitted',
'Cancelled', 'Inactive', 'Filled'):
ib.sleep(0.5)
if time.time() > deadline:
print("timeout waiting for order to settle")
break
print(f"Order settled: status={trade.orderStatus.status}")
# 6) 立即取消
if trade.orderStatus.status in ('Submitted', 'PreSubmitted'):
ib.cancelOrder(order)
print("cancel requested")
deadline = time.time() + 10
while trade.orderStatus.status not in ('Cancelled', 'Filled', 'Inactive'):
ib.sleep(0.5)
if time.time() > deadline:
print("timeout waiting for cancel")
break
# 7) 总结
print("---- final ----")
print(f"orderId={trade.order.orderId}")
print(f"status={trade.orderStatus.status}")
print(f"filled={trade.orderStatus.filled}")
print(f"log:")
for entry in trade.log:
print(f" {entry.time} {entry.status} {entry.message}")
# 8) 断开
ib.disconnect()
print("disconnected. done.")
if __name__ == '__main__':
main()
7.3 预期输出
Connecting to 127.0.0.1:7497 clientId=11 ...
Connected. server=176 account=['DUH123456']
SPY qualified: conId=756733
SPY ref price: 580.42, safe_limit (won't fill): 290.21
Order placed: orderId=1
[STATUS] orderId=1 status=PreSubmitted filled=0/1 avgFillPrice=0.0
[STATUS] orderId=1 status=Submitted filled=0/1 avgFillPrice=0.0
Order settled: status=Submitted
cancel requested
[STATUS] orderId=1 status=PendingCancel filled=0/1 avgFillPrice=0.0
[STATUS] orderId=1 status=Cancelled filled=0/1 avgFillPrice=0.0
---- final ----
orderId=1
status=Cancelled
filled=0
log:
2026-05-11 14:22:01.234 PendingSubmit -
2026-05-11 14:22:01.456 PreSubmitted -
2026-05-11 14:22:01.678 Submitted -
2026-05-11 14:22:02.123 PendingCancel -
2026-05-11 14:22:02.345 Cancelled -
disconnected. done.
7.4 这段脚本里的几个关键工程决定
| 决定 | 为什么 |
|---|---|
| 端口 assert | 防误连实盘——量化最贵的 bug 是连错环境。 |
| safe_limit = ref × 0.5 | 故意远离市价,确保不成交,否则 Paper 测试会真扣(虚拟)钱,破坏可重复性。 |
orderRef 带策略名 | 后期对账靠它 group。生产里推荐 <strategy>_<utc_ts>_<seq>。 |
| 状态轮询有 timeout | 网络抖动 / TWS 卡死时不挂死脚本。 |
| 事件 handler 在 connect 前注册 | 否则 connect 时的初始事件(2104/2106)会丢。 |
disconnectedEvent 监听 | 真实长跑里要触发重连;这里只打印。 |
八、PM 视角:今天学到的迁移性思考
-
同步外壳 + 异步内核是一种成熟的 SDK 模式。它让 90% 的用户写最简单的代码,又给 10% 的高阶用户留了 async 接口。我们做内部 SDK 时也该考虑:默认同步 + 提供 async 后门,比强制 async「政治正确」更友好。
-
Contract 对象 = 金融实体的强类型化。十年金融系统经验里,「symbol 是 string」是无数 bug 的根源——SPX vs SPXW、GOOG vs GOOGL、HK BABA vs US BABA。强类型 Contract + qualifyContracts 是行业沉淀出的最佳实践,迁移到任何金融系统都成立:永远不要让业务对象用 string 作主键。
-
状态机优于轮询。
Trade对象本质是个自更新的状态机视图,由事件流驱动。这是金融/支付/风控系统最该长成的样子——不是「定时去 query 状态」,而是「订阅状态变化」。我们做核心系统时,事件流(Kafka / NATS / 内部 EventBus)+ 状态投影比轮询数据库可靠 10 倍。 -
错误码 ≠ 异常。IBKR 的错误是通过事件流推送的「事实」而不是控制流的「中断」。这个抽象很值得学:不是所有「不正常」都该 throw——很多只是「需要业务感知」的事件。把它们建模成事件而不是异常,能让上层逻辑更清晰。
-
Bracket Order 是「把出场写死在入场时」的体现。这背后是更深的产品哲学:人在情绪低谷时做的决定最差,所以把出场决定前置到情绪好的时候。这条放在量化叫 pre-commitment,放在产品叫 default-by-design,放在监管叫 cooling-off period——都是同一件事。
九、Day 2 Checklist
按这个顺序做:
- (1) 把昨天的
tr_day1_connect.py跑一遍,确认 Paper 还能连 - (2) 阅读本笔记 § 一/二/三,理解同步外壳、Contract、订单类型矩阵
- (3) 在 TWS API Settings 里取消勾选 Read-Only API(昨天为了安全勾上的),保存
- (4) 把
tr_day2_roundtrip.py抄到本地,运行,看是否输出 Cancelled - (5) 故意把
safe_limit改成接近市价(ref + 0.01)的值,再跑一次,观察是否真的 Filled - (6) 故意把 PAPER_PORT 改成 7496,确认 assert 会拦下脚本
- (7) 改写
on_order_status,把每次状态变化追加写到orders.log文件 - (8) 在 ipython 里
from ib_insync import *; util.startLoop(),交互式跑一下ib.positions()/ib.accountSummary() - (9) 更新
docs/daily/TR_PROGRESS.mdDay 2 标 ✓ - (10) 在「实际执行记录」段补上踩到的坑
十、明日预告
Day 3:行情数据 — 实时报价、Tick、Bar、历史数据与 backfill
reqMktData流式 vssnapshot一次性- Tick types 详解:bid / ask / last / volume / 还有几十种你可能不知道的
reqTickByTickData:tick-by-tick(要订阅 enhanced data)reqHistoricalData:分钟/小时/日 bar,what to show(TRADES / MIDPOINT / BID / ASK)reqHistoricalTicks:拉历史 tick(最多 1000 个/次)- 数据持久化:用 parquet 存历史 bar,用 DuckDB 直接 query
- 第一个数据 pipeline:每天收盘后自动 backfill SPY 1m bar 到本地
实际执行记录
启动一项填一项,时间戳 + 卡点。
- [hh:mm] 取消 Read-Only API —
- [hh:mm] roundtrip 脚本第一次成功跑通 —
- [hh:mm] 故意成交版本(safe_limit 改近)—
- [hh:mm] 端口 assert 测试 —
- [hh:mm] 持仓/账户摘要交互式查询 —
- 卡点 / 学到的:
总字数:约 6,500 字 今日完成度:理论 ✓ / 实操(你自己执行)/ 笔记 ✓