tradingSystem/QMT_SIDE_S3_CLOSEOUT.md

9.9 KiB
Raw Blame History

给 QMT 侧S3 收尾的五件事2026-07-30

面向 QMT 侧研发。上一份是 QMT_TEST_ENV_REQUIREMENTS.mdR1R4 模拟环境需求), R1R4 已于今天上午验完,主体可以划掉——先说这个,避免重复投入。 本文只列剩下的五件事,按优先级排。第 1 条阻断切换,第 5 条最容易被忽略但后果最重。


0. 先说已经通了的(无需再动)

结果 实测依据
R1 可控成交 600000.SH sell 100 @10.13(涨跌停带内、高于市价)停在 SUBMITTED、成交 0。同代码同数量的 @99.99 昨天是秒成,行为确实改了
R2 代码校验 999999.SHreject{instruction_id, code:"UNKNOWN_CODE", reason:"instrument not found: 999999.SH"}。三点全对:带 instruction_id(委托级而非协议级)、码值取自协议 §7.3 表而非自造、reason 带原文。这一类做得很干净
R3 部分成交 PARTIAL → PARTIAL → FILLED 序列正确(见下面第 1 条,只有 trade_no 的格式要改)
R4 到期自动撤 valid_until 到点推 order_update status=EXPIREDleaves_qty 归零
主动撤单 SUBMITTED → CANCELLEDleaves_qty=0,与我方本地 cancel_state 一致
控制面不被业务阻塞 时钟差稳定在 +0.92 秒,最慢一条上行只比最快的晚 164 ms。ack_seq 挪出控制面那一改很有效——昨天那个 +30 秒的投递卡顿再没出现

协议 §9 的 S3「五类各至少一次」至此凑齐(全成 / 部分成交 / 主动撤单 / 到期过期 / 拒绝)。


1. trade_no 的格式不合协议 §5.5这一条阻断切换

实测收到的是:

trade_no: "T-SHADOW-6a21…"    ← 每笔一个随机串
trade_no: "T-SHADOW-9f64…"
trade_no: "T-SHADOW-772b…"

协议 §5.5 定的是 {broker_order_id}#{n},即同一张委托 SHADOW-7c472c48f0 的三笔应为:

SHADOW-7c472c48f0#1
SHADOW-7c472c48f0#2
SHADOW-7c472c48f0#3

为什么这不是格式洁癖。 PMS 侧有两层去重seq 水位§6.1+ trade_no 唯一索引§5.5)。 随机串当下能去重——唯一索引照样拦得住重复。问题在补发

按 §6.1 断线重连要重发 last_seq+1 之后的消息。如果贵方在补发时重新生成了随机 trade_no,同一笔成交就会带着两个不同的 trade_no 到达 PMS。第二层去重当场失效只剩 seq 一层;而 seq 那层在我方水位回退或 inbox 清理后也不存在了。结果是同一笔成交入账两次—— 持仓多算、摊薄成本算错,而摊薄成本是我方安全垫的分母,安全垫又是补仓/加仓/减仓的共同判据。

{broker_order_id}#{n}确定性的:委托号 + 第几笔,补发一万次都是同一个值。这正是协议 当初定这个格式的理由。

请确认:现在的 T-SHADOW-xxx 在补发时是原值重放,还是重新生成?如果是重新生成, 这条必须改完我们才敢切 PMS_DISPATCH_MODE=ws


2. 限价超出涨跌停区间:请回 BAD_PARAM

实测 600000.SH sell 100 @99.99(浦发涨停价约 11.1,挂到市价近 10 倍)既没被拒也没成交 一路挂到 EXPIRED

这一半是我们协议的锅,已经修了。 原 §7.3 拒绝码表里:

  • LIMIT_UP / LIMIT_DOWN 指的是「涨/跌停无法成交该方向」——价格合法,只是此刻撮不上, 所以 retryable=是(换价重发有意义)
  • BAD_PARAM 原文只写了「价格精度超限」
  • 「限价超出当日涨跌停区间」没有落点——贵方按表实现,找不到该回哪个码

协议已升 V1.0.1BAD_PARAM 的含义扩为「价格不合法——精度超限,或限价超出当日涨跌停 区间」,并补了与 LIMIT_UP/LIMIT_DOWN 的分界说明。消息集与状态机一字未动,贵方现有实现 不改也合规,只是多一个校验点。

请在模拟环境补上这个校验:越界限价 → reject{code:"BAD_PARAM", retryable:false}。 (顺带提一句:变更记录表的顺序我们也修了,原先 V1.0 那行排在 V0.9.2 上面,正是 ack_seq 被漏掉的原因。现在按版本号从旧到新排,读到最后一行才是当前版本。)


3. 请提供 07-29 的 signature verification failed 日志段

我方 pms_qmt_inbox 里有 19 条连续的

seq 6299  6317
{"instruction_id": null, "code": "SIG_INVALID",
 "reason": "signature verification failed", "retryable": false}

贵方验不过我方的签名,连续 19 条。签名是协议三条安全底座之一,这个数量不像偶发。

我方现在查不动了:那段 pms-ws 日志随容器重建消失,且 PMS 侧不留发送侧流水,所以无从知道 被拒的是哪 19 条消息。这很要紧——协议级 reject 我方不自动重发,那 19 条消息永久丢失。 如果里面有 place_ordercancel_order,那就是「发出去了没人执行」,而通道一路显示 ONLINE。

请帮忙查:贵方收到的原始帧一定在日志里。想知道两件事:

  1. 被拒的是哪些 typeplace_order / cancel_order / ack_seq / hello / ping
  2. 验签失败的直接原因——公钥读错?规范化串对不上?还是某类消息的字段序列化有差异?

一个我们比较怀疑的方向:带中文 noteplace_order。我方联调单的 note 是 「ws 联调测试单」UTF-8 处理只要有一点差异(转义、ensure_ascii、字节序)规范化串就对不上, 而其它不带中文的消息都正常。协议 §2.1.1 的测试向量建议双方再各跑一次比对。


4. _sender_loop 的队列与 socket 生命周期不一致(第三次提,仍未得答复)

QMT_SIDE_CONTROL_PATH.mdQMT_TEST_ENV_REQUIREMENTS.md §5.1 都提过:

sender = asyncio.create_task(self._sender_loop(websocket))   # socket 创建时绑死

async def _sender_loop(self, websocket):
    while True:
        env = await self._send_queue.get()      # 队列每轮重读实例属性
        await websocket.send(...)               # 发往绑死的那个 socket

self._send_queue 每次新连接会被重新赋值。旧连接的 sender 若尚未被 cancelfinally 是 异步执行的,close() 返回不代表旧任务已收尾),下一轮 get() 拿到的是新队列的消息,却 发往旧的已关闭 socketsend 抛异常 → break消息静默丢失,新连接那边在等一个 永远不来的回报。

建议把队列作为参数传入,与 socket 同生命周期:

queue = asyncio.Queue()
self._send_queue = queue
sender = asyncio.create_task(self._sender_loop(websocket, queue))

这条和第 3 条的现象可以互相解释(都是「消息发出去了但对端没正确处理」),所以一起看。


5. 重建模拟持仓时,cost_price 必须是真实成本价(最容易忽略,后果最重)

背景:贵方今天上午清空了 trading_position我方也把账本清干净了PMS 侧 07-30 11:14现在双方都是空的,等贵方装上持仓后我方按「以下游为准」认领重建。

请务必注意——trading_position 里的 cost_price 会直接变成我方账本的开仓价

  • 我方 ADD_RECON_LOT 的开仓价优先取 cost_price,取不到才拿现价兜底(并标注是估的)
  • cost_price 填 0、留空、或等于现价,我方摊薄成本就等于当天价、安全垫齐刷刷是 0
  • 安全垫是盈利加仓(≥3%)、保垫减仓(峰值≥6%)、补仓评估档(8%/15%)共同的判断依据 它一错,整条纪律链跟着错,页面和日报上的浮动盈亏也全是 0
  • 一只真实成本 20、现价 10 的票(实亏 50%)会被记成「不赚不亏」,该评估的补仓不评估

2026-07-29 首次接管 22 只持仓时踩过一次。我方取值优先级已有单测守着,但数据本身对不对 只能靠贵方available_quantityT+1 可卖)同理,请填合理值。

另外两件请提前通知我方的事:

  1. 重建模拟环境会不会重置 seq / 清空存上行消息的 Redis 目前我方水位 8052与贵方 自报 8045 在握手那一刻是对齐的。若贵方 seq 重置回小值而我方水位不动,就是序号倒挂—— 我方会把贵方此后发的每一条(含成交)都当成「不高于水位」静默丢弃,而通道显示一切正常。 这种情况必须双方商定同时归零,我方绝不会单方面下调水位(那会导致区间内成交重复入账)。
  2. 装持仓的时点。我方 15:10 的日终结算会自动认领 trading_position 的内容,且账本为空时 不设任何拦阻。所以贵方装到一半时如果正好跨过 15:10我方会拿半成品建账。装好了说一声 即可,我方在那之前会把调度停掉。

6. 已经闭环、不用再回的

  • trading_buy_plan 轮询已于 2026-07-30 10:55 停止 —— QMT_INTERFACE_REQUIREMENTS.md C2「旧通道停用清单」那个长期未决项至此关闭多谢。
  • nonce 16 字节§2.2)、ack_seq 累积确认§4.5、§6.1 序号补发、Ed25519 双向签名互通 —— 全部实测通过。

唯一还想确认的是 D3:贵方当前消费决策系统信号的全部位置是否都停了? C2 只覆盖了 trading_buy_plan 这一条路,「直接执行决策系统卖出指令与盘中 ENTRY/EXIT 信号」 那一条如果还有别的消费点,切换当天两套系统会对同一只票同时下单。


优先级建议

1阻断切换 > 5数据正确性做在装持仓之前 > 3安全底座 > 2 > 4

第 1 条改完 + 第 5 条按口径装好持仓,我方即可进入「小仓位实盘」;第 3 条不查明,签名这条 安全底座上就一直挂着一个问号。