tradingSystem/WS_INTEGRATION_STATUS.md

14 KiB
Raw Blame History

ws 联调进度与交接(截至 2026-07-30 上午)

给下一次接手的人(或下一个对话)用。三分钟读完就能接着干。 入口文档仍是 README.md / POSITION_MGMT_DESIGN.mdV0.4 定稿,勿改)/ QMT_WS_PROTOCOL.mdV1.0 定稿)。本文只记当前状态踩过的坑


1. 一句话状态

ws 通道全线打通并实测通过S3 五类已凑齐07-30 上午补完撤单/过期两类);PMS_DISPATCH_MODE 仍是 shadow,一次真实指令都没发过。卡在切换前的是三件事:对端 trade_no 不合 §5.5、 账本被停机遗留数据污染待重建、19 条 SIG_INVALID 未查明。


2. 已验证(实测,非推演)

依据
Ed25519 双向签名 与对端 crypto.py 逐字比对 + 双向验签实测
握手 / 心跳 / 重连退避 一天累计近百次重连seq 水位每次无缝续接
ack_seq 累积确认§4.5 对端补上实现后PMS 不再降级停发
§6.1 序号补发 ws_smoke rewind --by 60 人为造缺口,对端从 last_seq+1 起严格按序补回 60 条,约 15ms/条,一条不漏
委托全链路 place_order → ack → order_update(SUBMITTED) → trade → persist_result → order_update(FILLED) → funds_update,状态推进与字段全对
入账分流 只有 trade 进账本(processed=0),其余一律「已消化」
页面 ws 监控页签 真浏览器实测含告警渲染、pong 过滤、轮询起停)

07-30 上午补测(对端已上模拟环境的 R1/R2

结果 实测依据
R1 可控成交 600000.SH sell 100 @10.13(涨跌停带内、高于市价)停在 SUBMITTED、成交 0——同代码同数量的 @99.99 07-29 是秒成
主动撤单 CANCELLED seq 7089 SUBMITTED → 7106 CANCELLED;本地 cancel_state=SENT 与对端终态一致,无不一致告警
到期过期 EXPIRED --ttl 1 → seq 7124 SUBMITTED → 7137 EXPIRED
R2 代码校验 999999.SHreject{instruction_id, code=UNKNOWN_CODE, reason="instrument not found"};码值是协议 §7.3 表里的,带 instruction_id(委托级而非协议级),三点全对
部分成交 PARTIAL 07-29 已产出:INS-20260729-4ec5ce4c 走 seq 6028 PARTIAL → 6035 PARTIAL → 6040 FILLED
时钟与投递 min +868 ms = 真实时钟差约 0.87 秒,max-min 仅 74 ms——07-29 那个 +30213 ms 的事件循环卡顿没再出现,ack_seq 挪出控制面的改法生效了

S3 五类至此各至少一次(全成 / 部分成交 / 主动撤单 / 到期过期 / 拒绝)。 剩下的验收条件是日终对账零差异,以及下面 §5 的三件待办。

协议三条安全底座(签名、补发、对账兜底)前两条已齐——但签名那条被 §5 的 SIG_INVALID 划了个问号。


3. 当前状态与未决项

3.1 账本不是空的——是脏的07-30 更正)

07-29 那次事故15:10 daily_settlereconcile() 读到下游持仓表空集,按铁律 「以下游为准」把 22 个持仓、约 126 万市值全部核销)依然成立,机制也已由代码确认 reconcile 无条件应用 fixes且空结果集会跳过数量列校验。但清空之后账本又长出了东西

600000.SH  HOLDING  1100 股  avg_cost 9.273   ← 11 笔联调成交被判成「外部成交」并入 BASE
999999.SH  HOLDING  2000 股  avg_cost 9.274   ← 假代码, 防线部署前从下游对账补进来的

污染路径07-30 查明,不是推测):出口表 pms_qmt_order 现在只剩 7 行,而 inbox 里的 trade 引用了 4ec5ce4c / f0efcf41 / 43e47c39 三个出口表里根本没有的 instruction_id (共 11 笔)。consume_ws_trades 原先沿用 replay_fills 的口径「反查不到就按真单走,宁可 多入账也不能漏账」,于是这 11 笔走了真单分支 → 指令也查不到 → 判为外部成交并入 BASE。 SMOKE_ 那道闸压根没机会生效,因为它的判据正是「反查出口表」。

  • 14 条 trade 现在全部 processed=1(已入账),不是「已消化」
  • 根因是上下游都停了但遗留数据没清——停机遗留 ≠ 外部成交,这个缺口已在 §4 堵掉
  • 结论不变:接受损失。等对端装上像样持仓 → reset_ledger.py → 重新认领

3.2 下游只有垃圾

trading_position 现在只剩 999999.SH × 2000——我们自己测拒绝路径用的假代码。 现已被代码合法性校验过滤,不会再流进账本。

3.3 7 张风控 EXIT 已撤销

origin_type=system / origin_id=intraday / from_signal=true,理由「风控 SELL 置信度 95% ≥ 85%, 清仓」。持仓归零后指令悬空,已全部 cancel。不是用户下的命令。

3.4 组合基线(事故前,供重建时参考)

22 只 · 市值 1,258,357 / 成本 1,515,13916.9%)· 62.9% vs 60% 上限 · 22 家 vs 15 家上限 · 16 只 ≤ 15%。 688825.SH 成本 8.66 / 现价 48.5 是新股中签,数据正确,非异常。


4. 今天新加的防线(都有测试锁死)

防线 位置 起因
对账爆炸半径 ledger_service._recon_blast_guard 下游读空清了 22 个持仓
下游代码合法性 command_spec.is_stock_code 999999.SH 差点被补进账本
联调单不入账 ledger_service.consume_ws_trades + qmt_repo.SMOKE_PREFIX 对端按限价全成,每张测试单都在赌账本
卖出认领不到时告警 recon.map_trades_to_book 买入告警而卖出静默核销,不对称
补发期告警限流 runner._persist_upstream 一次补发刷 60 行 WARNING真乱序会被埋掉
时延采样跳过补发件 runner._track_skew 补发原样重放旧 ts误报 345 秒卡顿
孤儿成交挂起不入账07-30 ledger_service.consume_ws_trades + qmt_repo.PROC_ORPHAN 11 笔停机遗留成交被判成外部成交,账本长出 1100 股
反查报错时一笔都不判07-30 同上 原先把「库抖了」当「查不到」→ 当真单入账,一次超时就能造出一笔外部成交
挂起数字露出通道状态07-30 dispatcher.channel_status().orphan_held + ws_smoke status 挂起的行不再进 inbox_pending,没人报数就是一笔无人知晓的漏账

孤儿成交这条的判断依据值得记住的口径ws 这条路上的 trade 必带 instruction_id§5.5 而那个 id 是 PMS 自己生成、自己写进出口表的,所以反查不到只可能是数据不一致,不可能是 「有人在 QMT 手工下了单」——手工成交走 trading_order 那条路,根本不进 inbox。两种错的代价 并不对称:漏账看得见inbox 留着行、有 ERROR、通道状态报数字错账看不见(凭空长出来的 持仓与真持仓同形,而摊薄成本和安全垫已经全错,补仓/加仓/保垫减仓全挂在安全垫上)。所以不确定时 挂起等人工,不猜。processed=32(已消化)必须分开——2 的语义是「确认过不用管」,混在 一起事后就分不出哪些是真丢账。要追认:确认这些成交真该入账后,把那些行的 processed 改回 0


5. 待对端QMT 侧)

R1R4 已在 07-30 上午验完(结果见 §2R1、R2 的代码校验、R3、R4 均已到位 QMT_TEST_ENV_REQUIREMENTS.md 的主体可以划掉。剩下三件:

5.1 trade_no 不合协议 §5.5(阻断切换)

实测值是 T-SHADOW-6a21… / T-SHADOW-9f64…,协议要求 {broker_order_id}#{n}SHADOW-7c472c48f0#1/#2/#3。已核实 PMS 只是原样读 payload 的 trade_no ws_codec.py:418 / runner.py:488),不自造,所以是对端的格式。

为什么必须改:随机串当下能去重(唯一索引照样拦),但它不确定。§6.1 的补发场景里, 对端若重新生成一次随机串,同一笔成交就拿到两个不同的 trade_no → 第二层去重失效,只剩 seq 一层;而 seq 那层在水位回退或 inbox 被清后也没了 → 重复入账,摊薄成本与安全垫一起错{broker_order_id}#{n} 是确定性的,补发多少次都是同一个值——这正是 §5.5 当初定这个格式的理由。

5.2 价格带校验缺失

600000.SH sell 100 @99.99(浦发涨停约 11.1)既没被拒也没成交,一路挂到 EXPIRED。 根因一半在协议:原 §7.3 里 LIMIT_UP/LIMIT_DOWN 指的是「涨跌停无法成交该方向」, BAD_PARAM 只写了「价格精度超限」,「限价超出当日涨跌停区间」没有落点。 协议已升 V1.0.1BAD_PARAM 的含义扩为「价格不合法(精度超限或限价越界)」, 并补了与 LIMIT_UP/LIMIT_DOWN 的分界说明。消息集与状态机一字未动V1.0 的实现无需改动即合规。

5.3 19 条 SIG_INVALID 未查明(签名底座上的问号)

pms_qmt_inbox 里 seq 62996317 连续 19 条 reject{instruction_id: null, code: SIG_INVALID, reason: "signature verification failed"}——是对端验不过我们的签名

  • PMS 侧日志已随 force-recreate 消失,logs/docker compose logs pms-ws 都翻不到
  • 这 19 条消息永久丢了runner 对 reject 不自动重发。若其中有 place_ordercancel_order,那就是「发出去了没人执行」,而通道一路显示 ONLINE
  • 只能向 QMT 侧要 07-29 的 signature verification failed 日志段——他们收到的原始帧一定在
  • 顺带一个待补的能力PMS 侧不留发送侧流水,所以事后无从知道被拒的是哪几条。要不要加,见 §6

5.4 _sender_loop 生命周期(QMT_SIDE_CONTROL_PATH.md §5.1,仍未得答复)

队列绑定(每轮重读 self._send_queue)与 socket 绑定(创建时绑死)生命周期不一致,重连 频繁时旧 sender 会从新队列取消息发往已关闭 socket消息静默丢失。这条与 5.3 的现象可以 互相解释(都是「消息发出去了但对端没正确收到」),一起问更省事。


6. 下一步

  1. 确认对端实现了 R1R4 的哪几条 07-30 上午完成,结果见 §2
  2. 发函 QMT 侧§5.1 trade_no 改回 {broker_order_id}#{n}、§5.2 越界价按 BAD_PARAM 拒、 §5.3 索要 07-29 的 SIG_INVALID 日志段、§5.4 再确认一次
  3. 重建账本:对端装上像样持仓 → reset_ledger.py → 认领。注意现在账本是脏的不是空的§3.1
  4. 盘中scan-proposals?dry_run=true + exec-tick?dry_run=true,看真实候选清单
  5. 日终对账零差异 → S3 完整通过
  6. 两边都确认,再切 PMS_DISPATCH_MODE=ws

第 4 步必须在交易时段做,原因见下。第 3 步之前先跑一次 ws_smoke status挂起待人工 那个数字——07-30 那 11 笔已经入了账,新的孤儿成交才会被挂起。


7. 已知的坑(别再踩)

docker compose restart 不重载代码。 源码是 COPY . . 打进镜像的,restart 只是 重启老容器。改完代码要生效:./scripts/deploy.sh,或在开发机上 cp docker-compose.dev.yml docker-compose.override.yml 挂载源码(此后 restart 才有效, 但改 requirements.txt 仍需 build这个坑吃掉过一整轮排查。

出口表被清过(或换过环境)会造出「孤儿成交」。 inbox 里的 trade 带着 instruction_id 出口表里却没有对应行——SMOKE_ 隔离的判据正是反查出口表,所以那道闸对孤儿成交完全失效。 07-30 之前这类成交会被当成外部成交并入 BASE现在改为挂起processed=3+ ERROR 告警。 上下游停机但遗留数据没清时最容易撞上,重建账本前先看一眼 ws_smoke status 的「挂起待人工」。

pms_ws_state.last_seq 运行时手改无效。 两条机制会撤销它:_shutdown 会把内存水位 刷回库;_boot 会用 inbox_recover_watermark 顺着 inbox 连续段走回去。要造缺口用 ws_smoke rewind(它会拒绝在 pms-ws 运行时执行,也会拒绝跨过已入账的成交)。

对端模拟环境按限价全成、不校验代码。 一张 99.99 的浦发卖单(实际价 10 元)会全成, 999999.SH 也会全成。任何测试单都会产生成交回报——所以联调单隔离(SMOKE_ 前缀) 是必需的,不是可选的。

收盘后的 dry run 看不出东西。 取不到实时价时 positions_view 用摊薄成本兜底当现价, 安全垫齐刷刷归零DCA/ADD/TRIM 三个评估器全不触发,结果永远是 candidates: 0。 判断"切了 ws 会发生什么"必须在交易时段跑。

pong 带 seq符合 §5 5 秒一条、一天约 1.7 万行进 pms_qmt_inbox。 看 inbox 记得用 ws_smoke inbox --type trade 过滤,否则全是 pong。

协议 reject 不带 instruction_id 时是「协议级拒绝」——拒的是消息本身不是委托, 走完全不同的分支。对端曾因未实现 ack_seq 而回 unknown type ack_seq,形成 「我们 ack → 对端拒 → 拒绝带 seq → 我们又要 ack」的 2 秒自激循环,通道显示 ONLINE 但只在刷 reject。


8. 文档欠账

已清2026-07-30用户批准QMT_WS_PROTOCOL.md §11 变更记录里 V1.0 那行原本 排在 V0.9.2 上面,而 ack_seq 恰恰是 V0.9.2 补进来的——对端从上往下读到「V1.0 定稿」 就停,ack_seq 因此被漏掉,直接导致了 §7 那个自激循环。现已改为 V0.9 → V0.9.1 → V0.9.2 → V1.0。纯调两行顺序,条款一字未动(总行数不变,可 git show 核)。

遗留一项(待定夺,暂不动):端点在 README.md「ws 通道实现清单」与协议 §11 的 V1.0 那行里都写作 ws://192.168.16.98:8080,而 config/settings.py:94 实际是 ws://192.168.16.98:9443/pms(提交 0a1a932 调整端口)。代码是对的、文档是旧的 联调不受影响(运行时读 settings / 运行参数,不读文档),但下一个接手的人会按文档去连 8080。 改的是定稿协议正文里的一处事实值,故未擅动。