tradingSystem/WS_INTEGRATION_STATUS.md

216 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ws 联调进度与交接(截至 2026-07-30 上午)
> 给下一次接手的人(或下一个对话)用。三分钟读完就能接着干。
> 入口文档仍是 `README.md` / `POSITION_MGMT_DESIGN.md`V0.4 定稿,勿改)/
> `QMT_WS_PROTOCOL.md`V1.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.SH``reject{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_settle``reconcile()` 读到**下游持仓表空集**,按铁律
「以下游为准」把 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=3` 与 `2`(已消化)必须分开——`2` 的语义是「确认过不用管」,混在
一起事后就分不出哪些是真丢账。要追认:确认这些成交真该入账后,把那些行的 `processed` 改回 `0`
---
## 5. 待对端QMT 侧)
R1R4 已在 07-30 上午验完(结果见 §2。**R1、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.1**`BAD_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_order`
`cancel_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。
改的是定稿协议正文里的一处事实值,故未擅动。